#[cfg(test)]
mod tests {
use super::super::auto_connect_impl::{
active_connection_probe_decision, active_connection_probe_decision_with_independent_route,
retryable_session_admission_failure_decision, session_admission_presentation_decision,
session_token_presentation_failure_action, session_token_presentation_was_duplicate,
should_auto_present_session_token, ActiveConnectionProbeDecision,
RetryableSessionAdmissionFailureDecision, SessionTokenPresentationFailureAction,
};
use super::super::core_impl::{
accepted_transport_event_is_current, classify_wasm_closed_transport_rebind,
incoming_transport_requires_optional_route_restart,
is_duplicate_kept_existing_close_reason, is_graceful_disconnect_close_reason,
is_manual_disconnect_close_reason, is_replacement_churn_close_reason,
is_terminal_disconnect_close_reason, report_iroh_path_observation_if_current,
session_admission_timeout_owns_current_transport, WasmClosedTransportRebind,
};
use super::super::state_signaling_impl::{
should_present_session_token_on_connected_transport,
wasm_independent_route_proof_is_current,
};
#[cfg(all(not(target_arch = "wasm32"), feature = "transport-moq"))]
use super::super::MoQConfig;
use super::super::{
auto_connect_admission_rejection_backoff_ms, auto_connect_failure_backoff_ms,
auto_connect_tie_break_decision, clear_auto_connect_failure_state,
duplicate_closed_handling, incoming_local_close_should_wait_for_replacement,
merge_device_status_snapshots, register_auto_connect_admission_rejection,
register_auto_connect_failure, resolve_disable_ipv6, AutoConnectTieBreakDecision,
BleConfig, Client, DeviceConnectionStatus, DevicePresenceStatus, DuplicateClosedHandling,
IncomingTransportCloseResolution, IrohPathKind, ManagedConnectionBridgeAction,
ManagedConnectionHealthStatus, ReadinessState, RemoteSessionAdmissionProof,
TransportConfig, WebRTCConfig, MANAGED_SETTLE_DEADLINE_MS,
};
use crate::connection_manager::{ConnectionHealth, ConnectionState, PeerSnapshot};
#[cfg(all(feature = "transport-lan", not(target_arch = "wasm32")))]
use crate::native_node::IncomingStreamType;
use crate::native_protocol::{InspectedMainFrame, NativeMainMessage, NativeMessageAction};
#[cfg(all(feature = "transport-webrtc", not(target_arch = "wasm32")))]
use crate::native_webrtc_policy::NativeWebRTCAttemptReservation;
use crate::signaling::Device;
use iroh::{Endpoint, RelayMode};
use std::collections::HashMap;
use tokio::io::AsyncWriteExt;
use tokio::sync::oneshot;
use tokio::time::{sleep, Duration};
#[cfg(not(target_arch = "wasm32"))]
fn establish_current_managed_user_device_route(
client: &Client,
connection_id: &str,
transport_stable_id: u64,
) {
client.bind_inbound_native_admission_stream_contract(
connection_id,
crate::native_protocol::SessionTokenStreamContract::PersistentControl,
);
client.bind_outbound_native_admission_stream_contract(
connection_id,
crate::native_protocol::SessionTokenStreamContract::PersistentControl,
);
client.bind_inbound_session_token_transport(connection_id, transport_stable_id);
client
.remote_session_admission_proofs
.write()
.expect("remote proof registry")
.insert(
connection_id.to_string(),
RemoteSessionAdmissionProof {
transport_stable_id,
token_fingerprint: "test-current-route-proof".to_string(),
approval_scope: "user-device".to_string(),
},
);
}
#[cfg(not(target_arch = "wasm32"))]
fn optional_route_generations(
route: crate::route_policy::KnownRoute,
generation: crate::client::NativeOptionalRouteGeneration,
) -> crate::client::NativeOptionalRouteGenerations {
let mut routes = crate::client::NativeOptionalRouteGenerations::default();
assert!(routes.set(route, Some(generation)));
routes
}
#[test]
fn transient_network_reconnect_reason_is_not_terminal() {
let reason = "ApplicationClosed(0, b\"network-change-forced-reconnect\")";
assert!(crate::lifecycle_reason::reason_is_transient_reconnect(
Some(reason)
));
assert!(!crate::lifecycle_reason::reason_is_graceful_disconnect(
Some(reason)
));
assert!(!crate::lifecycle_reason::reason_is_manual_disconnect(Some(
reason
)));
}
#[cfg(all(not(target_arch = "wasm32"), feature = "transport-moq"))]
#[test]
fn moq_config_deserializes_but_never_serializes_or_debugs_access_tokens() {
let token = "header.payload.signature";
let config: MoQConfig = serde_json::from_value(serde_json::json!({
"relayUrl": "https://relay.example.com/moq?diagnostic=private",
"accessToken": token,
}))
.expect("MoQ config with token should deserialize");
assert_eq!(config.access_token.as_deref(), Some(token));
let serialized = serde_json::to_string(&config).expect("MoQ config should serialize");
assert!(!serialized.contains(token));
assert!(!serialized.contains("accessToken"));
let debug = format!("{config:?}");
assert!(!debug.contains(token));
assert!(!debug.contains("diagnostic=private"));
assert!(debug.contains("[redacted]"));
}
#[test]
fn session_token_transport_loss_is_retryable_but_explicit_denial_is_terminal() {
for error in [
"[session-token-response:protocol-byte] connection lost",
"[session-token-response:protocol-byte] stream finished early (0 bytes read)",
"[session-token-response:protocol-byte] peer stream ended before 4 bytes were read",
"[session-token-response:protocol-byte] connection closed by peer",
"[session-token-response:protocol-byte] stream reset by peer",
"[session-token-response:protocol-byte] stream stopped by peer",
"[session-token-response:timeout] no host response within 5000ms",
"failed to open bi-stream for session-token presentation: disconnected",
"failed to write session-token presentation: connection lost",
"missing connection for session-token presentation: conn-1",
"[session-token-presentation:in-flight] another presenter owns conn-1",
] {
assert_eq!(
session_token_presentation_failure_action(error),
SessionTokenPresentationFailureAction::RetryOnNextTransport,
"{error} must not be treated as authorization denial",
);
}
assert_eq!(
session_token_presentation_failure_action("session-token-rejected"),
SessionTokenPresentationFailureAction::RejectAndDisconnect,
);
assert!(session_token_presentation_was_duplicate(
"[session-token-presentation:in-flight] another presenter owns conn-1"
));
assert!(!session_token_presentation_was_duplicate(
"[session-token-response:timeout] no host response"
));
}
#[test]
fn retryable_admission_failure_preserves_once_then_retires_same_generation() {
assert_eq!(
retryable_session_admission_failure_decision(1, 2, Some(41), Some(41)),
RetryableSessionAdmissionFailureDecision::PreserveCurrentTransport,
);
assert_eq!(
retryable_session_admission_failure_decision(2, 2, Some(41), Some(41)),
RetryableSessionAdmissionFailureDecision::RetireCurrentTransport,
);
}
#[test]
fn retryable_admission_failure_cannot_retire_replacement_generation() {
assert_eq!(
retryable_session_admission_failure_decision(2, 2, Some(41), Some(42)),
RetryableSessionAdmissionFailureDecision::ReplacementAlreadyWon,
);
assert_eq!(
retryable_session_admission_failure_decision(2, 2, Some(41), None),
RetryableSessionAdmissionFailureDecision::ReplacementAlreadyWon,
);
}
#[test]
fn accepted_transport_event_cannot_promote_a_stale_generation() {
assert!(accepted_transport_event_is_current(42, Some(42)));
assert!(!accepted_transport_event_is_current(41, Some(42)));
assert!(!accepted_transport_event_is_current(41, None));
}
#[test]
fn incoming_transport_restarts_optional_routes_for_new_or_replaced_generations() {
assert!(incoming_transport_requires_optional_route_restart(
false, false
));
assert!(incoming_transport_requires_optional_route_restart(
false, true
));
assert!(incoming_transport_requires_optional_route_restart(
true, true
));
assert!(!incoming_transport_requires_optional_route_restart(
true, false
));
}
#[test]
fn wasm_close_rebind_requires_a_concrete_base_transport_generation() {
assert_eq!(
classify_wasm_closed_transport_rebind(false, Some(42), true),
WasmClosedTransportRebind::BaseReplacement(42),
);
assert_eq!(
classify_wasm_closed_transport_rebind(false, None, true),
WasmClosedTransportRebind::IndependentTransportOnly,
);
assert_eq!(
classify_wasm_closed_transport_rebind(false, None, false),
WasmClosedTransportRebind::NoLiveTransport,
);
assert_eq!(
classify_wasm_closed_transport_rebind(true, Some(42), true),
WasmClosedTransportRebind::TerminalDisconnect,
"terminal security closes must outrank every surviving transport",
);
}
#[test]
fn admission_timeout_cannot_retire_after_late_approval() {
assert!(!session_admission_timeout_owns_current_transport(
false,
false,
41,
Some(41),
Some(41),
));
}
#[test]
fn admission_timeout_cannot_retire_replacement_transport() {
assert!(!session_admission_timeout_owns_current_transport(
true,
false,
41,
Some(42),
Some(42),
));
assert!(session_admission_timeout_owns_current_transport(
true,
false,
42,
Some(42),
Some(42),
));
}
#[test]
fn admission_timeout_cannot_retire_while_response_is_in_flight() {
assert!(!session_admission_timeout_owns_current_transport(
true,
true,
42,
Some(42),
Some(42),
));
}
#[test]
fn relay_only_endpoint_addr_strips_ip_addresses() {
let secret = iroh::SecretKey::generate();
let relay_url: iroh::RelayUrl = "https://relay.example.com".parse().unwrap();
let ip_addr = "192.168.1.20:4433".parse().unwrap();
let addr = iroh::EndpointAddr::new(secret.public())
.with_relay_url(relay_url.clone())
.with_ip_addr(ip_addr);
let scrubbed = Client::relay_only_endpoint_addr(addr).expect("relay address");
assert_eq!(
scrubbed.relay_urls().cloned().collect::<Vec<_>>(),
vec![relay_url]
);
assert_eq!(scrubbed.ip_addrs().count(), 0);
assert!(scrubbed.addrs.iter().all(|addr| addr.is_relay()));
}
#[test]
fn relay_only_endpoint_addr_fails_closed_without_relay() {
let secret = iroh::SecretKey::generate();
let addr = iroh::EndpointAddr::new(secret.public())
.with_ip_addr("10.0.0.12:4433".parse().unwrap());
let error = Client::relay_only_endpoint_addr(addr).unwrap_err();
assert!(error.to_string().contains("no relay address"));
}
#[test]
fn endpoint_shaping_transport_changes_require_rebind() {
let current = TransportConfig::default();
let mut relay_only = current.clone();
relay_only.iroh_relay_only = true;
assert!(current.endpoint_rebind_required(&relay_only));
let mut lan_disabled = current.clone();
lan_disabled.iroh_lan = None;
assert!(current.endpoint_rebind_required(&lan_disabled));
let mut dynamic_upgrade = current.clone();
dynamic_upgrade.webrtc = Some(WebRTCConfig::default());
assert!(!current.endpoint_rebind_required(&dynamic_upgrade));
}
#[tokio::test]
async fn moq_empty_config_requires_explicit_relay_url() {
let config: TransportConfig =
serde_json::from_value(serde_json::json!({ "moq": {} })).unwrap();
let client = Client::new_with_app_tag(
crate::test_constants::TEST_PROJECT_ID.to_string(),
"test-app".to_string(),
Box::new(|| None),
);
let error = client.update_transport_config(config).await.unwrap_err();
assert!(error.to_string().contains("relayUrl is required"));
}
#[cfg(not(target_arch = "wasm32"))]
#[test]
fn native_auth_builder_supplies_standalone_token_provider() {
let auth = crate::native_auth::NativeAuthState::new();
auth.set_session(crate::native_auth::FirebaseAuthSession {
id_token: "id-token".to_string(),
refresh_token: Some("refresh-token".to_string()),
expires_in_seconds: Some(3600),
expires_at_ms: None,
user_id: Some("user-123".to_string()),
auth_kind: crate::native_auth::FirebaseAuthKind::Anonymous,
});
let client = Client::builder_with_native_auth(
crate::test_constants::TEST_PROJECT_ID.to_string(),
crate::test_constants::TEST_API_KEY.to_string(),
auth,
)
.build();
assert_eq!((client.token_provider)().as_deref(), Some("id-token"));
assert_eq!(
client.app_tag(),
crate::app_tag_from_api_key(crate::test_constants::TEST_API_KEY).as_str()
);
}
#[cfg(not(target_arch = "wasm32"))]
#[test]
fn native_space_auth_builder_derives_space_app_tag() {
let auth = crate::native_auth::NativeAuthState::new();
auth.set_session(crate::native_auth::FirebaseAuthSession {
id_token: "space-id-token".to_string(),
refresh_token: Some("refresh-token".to_string()),
expires_in_seconds: Some(3600),
expires_at_ms: None,
user_id: Some("space-user".to_string()),
auth_kind: crate::native_auth::FirebaseAuthKind::Space,
});
let client = Client::builder_with_native_space_auth(
crate::test_constants::TEST_PROJECT_ID.to_string(),
crate::test_constants::TEST_API_KEY.to_string(),
"shared-space".to_string(),
auth,
)
.build();
assert_eq!((client.token_provider)().as_deref(), Some("space-id-token"));
assert_eq!(
client.app_tag(),
crate::space_app_tag_from_keys(crate::test_constants::TEST_API_KEY, "shared-space")
.as_str()
);
}
#[tokio::test]
async fn update_transport_config_sanitizes_unavailable_ble() {
let client = Client::new_with_app_tag(
crate::test_constants::TEST_PROJECT_ID.to_string(),
"test-app".to_string(),
Box::new(|| None),
);
client
.update_transport_config(TransportConfig {
ble: Some(BleConfig {
enabled: true,
connect_timeout_ms: Some(20_000),
}),
..TransportConfig::default()
})
.await
.expect("unsupported host transports should be sanitized, not fatal");
assert!(client.transport_config().await.ble.is_none());
let status = client.runtime_status().await;
assert!(!status.transport.ble.compiled);
assert!(!status.transport.ble.enabled);
}
#[cfg(not(target_arch = "wasm32"))]
#[tokio::test]
async fn registered_custom_ble_transport_preserves_config_and_reports_capability() {
let client = Client::new_with_app_tag(
crate::test_constants::TEST_PROJECT_ID.to_string(),
"test-app".to_string(),
Box::new(|| None),
);
client
.register_native_custom_transport_kind(0x42_4c_45, IrohPathKind::Ble)
.await
.expect("register BLE custom transport");
client
.update_transport_config(TransportConfig {
ble: Some(BleConfig {
enabled: true,
connect_timeout_ms: Some(20_000),
}),
..TransportConfig::default()
})
.await
.expect("registered BLE config");
assert!(client
.transport_config()
.await
.ble
.as_ref()
.is_some_and(|ble| ble.enabled));
let status = client.runtime_status().await;
assert!(status.transport.ble.compiled);
assert!(status.transport.ble.enabled);
}
#[tokio::test]
async fn is_connected_accepts_live_independent_transport_without_base_iroh() {
let client = Client::new_with_app_tag(
crate::test_constants::TEST_PROJECT_ID.to_string(),
"test-app".to_string(),
Box::new(|| None),
);
let endpoint_id: iroh::EndpointId =
"5d388fcaaa61efa536d78051e060928f6c1fff0983a32f6b532176b12328c3ac"
.parse()
.expect("endpoint id");
let endpoint = endpoint_id.to_string();
client
.connection_manager
.upsert_pending(
"conn-independent-webrtc".to_string(),
Some(endpoint.clone()),
Some("device-independent-webrtc".to_string()),
Some(endpoint.clone()),
)
.await;
client
.connection_manager
.set_connected_with_transport(
"conn-independent-webrtc",
Some(endpoint.clone()),
None,
Some("transient-disconnect".to_string()),
)
.await
.expect("connected record");
client
.connection_manager
.report_transport_status(
"conn-independent-webrtc",
"webrtc".to_string(),
Some("iroh".to_string()),
)
.await
.expect("transport status");
client
.connection_manager
.mark_transport_replaced(
"conn-independent-webrtc",
None,
Some("transient-disconnect".to_string()),
Some(crate::lifecycle_reason::REASON_REPLACEMENT_IN_PROGRESS.to_string()),
)
.await
.expect("replacement marker");
assert!(
client.is_connected(endpoint_id).await,
"connected independent WebRTC route should satisfy high-level connected semantics even when the base Iroh connection is absent",
);
}
async fn wait_for_endpoint_addr(
endpoint: &Endpoint,
timeout_ms: u64,
) -> anyhow::Result<iroh::EndpointAddr> {
let started = std::time::Instant::now();
loop {
let addr = endpoint.addr();
if !addr.is_empty() {
return Ok(addr);
}
if started.elapsed() > Duration::from_millis(timeout_ms) {
return Err(anyhow::anyhow!(
"endpoint {} did not publish a dialable address within {}ms",
endpoint.id(),
timeout_ms
));
}
sleep(Duration::from_millis(50)).await;
}
}
#[cfg(feature = "transport-lan")]
#[tokio::test]
async fn native_iroh_loopback_connect_establishes_direct_lan_path() {
let test_started = std::time::Instant::now();
let client_a = Client::new_with_app_tag(
crate::test_constants::TEST_PROJECT_ID.to_string(),
"lan-e2e-a".to_string(),
Box::new(|| None),
);
let client_b = Client::new_with_app_tag(
crate::test_constants::TEST_PROJECT_ID.to_string(),
"lan-e2e-b".to_string(),
Box::new(|| None),
);
let endpoint_a = Endpoint::builder(iroh::endpoint::presets::N0)
.alpns(vec![crate::native_node::PlutoniumProtocol::ALPN.to_vec()])
.relay_mode(RelayMode::Disabled)
.bind_addr(
"127.0.0.1:0"
.parse::<std::net::SocketAddr>()
.expect("socket addr a"),
)
.expect("bind addr a")
.bind()
.await
.expect("bind endpoint a");
let endpoint_b = Endpoint::builder(iroh::endpoint::presets::N0)
.alpns(vec![crate::native_node::PlutoniumProtocol::ALPN.to_vec()])
.relay_mode(RelayMode::Disabled)
.bind_addr(
"127.0.0.1:0"
.parse::<std::net::SocketAddr>()
.expect("socket addr b"),
)
.expect("bind addr b")
.bind()
.await
.expect("bind endpoint b");
client_a.adopt_endpoint(endpoint_a).await;
client_b.adopt_endpoint(endpoint_b).await;
let endpoint_b = client_b.get_endpoint().await.expect("endpoint b");
let endpoint_b_id = endpoint_b.id();
let endpoint_b_addr = wait_for_endpoint_addr(&endpoint_b, 5_000)
.await
.expect("endpoint b addr");
let incoming = client_b.incoming_streams().await.expect("incoming streams");
client_a
.ensure_connected_addr(endpoint_b_id, endpoint_b_addr)
.await
.expect("connect over loopback LAN");
let connected_after = test_started.elapsed();
tokio::time::timeout(Duration::from_secs(5), async {
loop {
let kind = client_a.iroh_path_kind(&endpoint_b_id.to_string()).await;
if kind == crate::client::IrohPathKind::DirectLan {
break;
}
sleep(Duration::from_millis(50)).await;
}
})
.await
.expect("iroh selected path should become direct LAN");
let direct_after = test_started.elapsed();
let transport_rtt_ms = client_a
.iroh_transport_rtt_ms(&endpoint_b_id.to_string())
.await
.expect("direct LAN path should expose iroh transport RTT");
assert!(
(1..250).contains(&transport_rtt_ms),
"unexpected direct LAN transport RTT: {transport_rtt_ms}ms"
);
assert!(
direct_after < Duration::from_secs(5),
"direct LAN path settlement took {:?} after connect completed at {:?}",
direct_after,
connected_after,
);
let connection = client_a
.get_connection(endpoint_b_id)
.await
.expect("active connection");
let (mut send, _recv) = connection.open_bi().await.expect("open bi stream");
send.write_all(b"lan-e2e").await.expect("write payload");
send.finish().expect("finish stream");
let incoming_stream = tokio::time::timeout(Duration::from_secs(5), incoming.recv())
.await
.expect("incoming stream timeout")
.expect("incoming stream");
assert!(matches!(
incoming_stream.stream,
IncomingStreamType::Bi(_, _)
));
}
#[tokio::test]
async fn send_peer_rejects_bulk_payloads_before_transport_lookup() {
let client = Client::new_with_app_tag(
crate::test_constants::TEST_PROJECT_ID.to_string(),
"test-app".to_string(),
Box::new(|| None),
);
let payload = vec![0u8; crate::native_protocol::MAX_PEER_MESSAGE_BYTES + 1];
let error = client
.send_peer("missing-peer", &payload)
.await
.expect_err("bulk payload must use a named stream");
assert!(error.to_string().contains("use a named stream"));
}
#[tokio::test]
async fn iroh_path_handoffs_repromote_direct_routes_without_replacing_the_physical_leg() {
let client = Client::new_with_app_tag(
crate::test_constants::TEST_PROJECT_ID.to_string(),
"test-app".to_string(),
Box::new(|| None),
);
let connection_id = "conn-iroh-path-handoffs";
let remote_node_id = "node-iroh-path-handoffs";
let stable_id = 73;
client
.connection_manager
.upsert_pending(
connection_id.to_string(),
Some(remote_node_id.to_string()),
Some("device-iroh-path-handoffs".to_string()),
Some(remote_node_id.to_string()),
)
.await;
client
.connection_manager
.set_connected_with_transport(
connection_id,
Some(remote_node_id.to_string()),
Some(stable_id),
Some("path-handoff-test".to_string()),
)
.await
.expect("connected record");
let initial_transport_generation = client
.connection_manager
.get_by_connection_id(connection_id)
.await
.expect("initial path handoff record")
.transport_generation;
let mut previous_route_generation = 0;
for (path_kind, expected_transport) in [
(
crate::client::IrohPathKind::Relay,
crate::transport_label::IROH_RELAY,
),
(
crate::client::IrohPathKind::DirectLan,
crate::transport_label::IROH_LAN,
),
(
crate::client::IrohPathKind::Relay,
crate::transport_label::IROH_RELAY,
),
(
crate::client::IrohPathKind::DirectQuic,
crate::transport_label::IROH_QUIC,
),
] {
crate::client::core_impl::handle_path_change(
&client,
connection_id,
remote_node_id,
stable_id,
path_kind,
false,
)
.await;
let record = client
.connection_manager
.get_by_connection_id(connection_id)
.await
.expect("path handoff record");
assert_eq!(record.active_transport, expected_transport);
assert_eq!(record.transport_stable_id, Some(stable_id));
assert_eq!(
record.transport_generation, initial_transport_generation,
"an in-place selected-path handoff must preserve the physical transport generation"
);
assert!(
record.route_generation > previous_route_generation,
"each selected-path handoff should advance route generation"
);
previous_route_generation = record.route_generation;
}
crate::client::core_impl::handle_path_change(
&client,
connection_id,
remote_node_id,
stable_id,
crate::client::IrohPathKind::DirectQuic,
false,
)
.await;
let unchanged = client
.connection_manager
.get_by_connection_id(connection_id)
.await
.expect("unchanged direct route record");
assert_eq!(
unchanged.active_transport,
crate::transport_label::IROH_QUIC
);
assert_eq!(unchanged.transport_stable_id, Some(stable_id));
assert_eq!(
unchanged.transport_generation, initial_transport_generation,
"a duplicate selected-path event must preserve the physical transport generation"
);
assert_eq!(
unchanged.route_generation, previous_route_generation,
"a duplicate selected-path event must not manufacture route churn"
);
}
#[tokio::test]
#[cfg(not(target_arch = "wasm32"))]
async fn delayed_old_iroh_path_observation_is_rejected_after_physical_replacement() {
let client = Client::new_with_app_tag(
crate::test_constants::TEST_PROJECT_ID.to_string(),
"test-app".to_string(),
Box::new(|| None),
);
let connection_id = "conn-delayed-old-iroh-path";
let old_stable_id = 101;
let new_stable_id = 202;
client
.connection_manager
.upsert_pending(
connection_id.to_string(),
Some("node-delayed-old-iroh-path".to_string()),
Some("device-delayed-old-iroh-path".to_string()),
Some("endpoint-delayed-old-iroh-path".to_string()),
)
.await;
client
.connection_manager
.set_connected_with_transport(
connection_id,
Some("endpoint-delayed-old-iroh-path".to_string()),
Some(old_stable_id),
Some("incoming".to_string()),
)
.await
.expect("old physical transport should be connected");
let old_record = client
.connection_manager
.get_by_connection_id(connection_id)
.await
.expect("old connection record");
let (release_observer, observer_released) = oneshot::channel();
let observer_client = client.clone();
let delayed_observation = tokio::spawn(async move {
observer_released
.await
.expect("replacement should release the delayed observer");
report_iroh_path_observation_if_current(
&observer_client,
connection_id,
old_stable_id,
old_record.transport_generation,
old_record.route_generation,
"iroh-relay",
None,
)
.await
});
client
.connection_manager
.mark_transport_replaced_by_if_current(
connection_id,
old_stable_id,
new_stable_id,
Some("test-replacement".to_string()),
)
.await
.expect("replacement should install the new physical transport");
release_observer
.send(())
.expect("delayed observer should still be waiting");
assert!(
!delayed_observation
.await
.expect("delayed observer task should complete"),
"an old path watcher must not project onto a replacement transport"
);
let current = client
.connection_manager
.get_by_connection_id(connection_id)
.await
.expect("replacement connection record");
assert_eq!(current.transport_stable_id, Some(new_stable_id));
assert_eq!(
current.transport_generation,
old_record.transport_generation + 1
);
assert_eq!(current.route_generation, 0);
assert_eq!(current.active_transport, old_record.active_transport);
}
#[cfg(not(target_arch = "wasm32"))]
#[tokio::test]
async fn delayed_ble_replacement_completion_rejects_stale_generation_before_commit() {
let manager = crate::connection_manager::ConnectionManager::new();
manager
.upsert_pending(
"ble-generation-fence".to_string(),
Some("remote-node".to_string()),
Some("remote-device".to_string()),
Some("remote-node".to_string()),
)
.await;
let base = manager
.set_connected_with_transport(
"ble-generation-fence",
Some("remote-node".to_string()),
Some(101),
Some(crate::transport_label::IROH_RELAY.to_string()),
)
.await
.expect("base generation");
let winner = manager
.install_transport_replacement_if_generation_current(
"ble-generation-fence",
101,
base.transport_generation,
base.route_generation,
202,
Some("ble-upgrade".to_string()),
)
.await
.expect("current replacement");
assert_eq!(winner.transport_stable_id, Some(202));
assert!(
manager
.install_transport_replacement_if_generation_current(
"ble-generation-fence",
101,
base.transport_generation,
base.route_generation,
303,
Some("delayed-ble-upgrade".to_string()),
)
.await
.is_none(),
"the prior base generation cannot install a second replacement",
);
}
#[test]
fn auto_connect_failure_backoff_schedule_is_bounded() {
assert_eq!(auto_connect_failure_backoff_ms(0), 0);
assert_eq!(auto_connect_failure_backoff_ms(1), 1_000);
assert_eq!(auto_connect_failure_backoff_ms(2), 2_000);
assert_eq!(auto_connect_failure_backoff_ms(3), 4_000);
assert_eq!(auto_connect_failure_backoff_ms(4), 8_000);
assert_eq!(auto_connect_failure_backoff_ms(20), 8_000);
assert_eq!(auto_connect_admission_rejection_backoff_ms(1), 2_000);
assert_eq!(auto_connect_admission_rejection_backoff_ms(2), 4_000);
assert_eq!(auto_connect_admission_rejection_backoff_ms(3), 8_000);
assert_eq!(auto_connect_admission_rejection_backoff_ms(4), 16_000);
assert_eq!(auto_connect_admission_rejection_backoff_ms(5), 30_000);
assert_eq!(auto_connect_admission_rejection_backoff_ms(20), 30_000);
}
#[cfg(not(target_arch = "wasm32"))]
#[test]
fn native_ip_transport_defaults_to_dual_stack() {
assert!(!resolve_disable_ipv6(None, None));
assert!(resolve_disable_ipv6(Some(true), None));
assert!(!resolve_disable_ipv6(Some(false), Some(false)));
assert!(!resolve_disable_ipv6(None, Some(true)));
assert!(resolve_disable_ipv6(None, Some(false)));
}
#[test]
fn auto_connect_failure_state_registers_and_clears() {
let mut failure_count: HashMap<String, u8> = HashMap::new();
let mut backoff_until: HashMap<String, i64> = HashMap::new();
let remote = "remote-device-1";
let now = 1_000_i64;
register_auto_connect_failure(remote, now, &mut failure_count, &mut backoff_until);
assert_eq!(failure_count.get(remote).copied(), Some(1));
assert_eq!(backoff_until.get(remote).copied(), Some(now + 1_000));
register_auto_connect_failure(remote, now + 10, &mut failure_count, &mut backoff_until);
assert_eq!(failure_count.get(remote).copied(), Some(2));
assert_eq!(backoff_until.get(remote).copied(), Some(now + 10 + 2_000));
clear_auto_connect_failure_state(remote, &mut failure_count, &mut backoff_until);
assert!(!failure_count.contains_key(remote));
assert!(!backoff_until.contains_key(remote));
register_auto_connect_admission_rejection(
remote,
now,
&mut failure_count,
&mut backoff_until,
);
assert_eq!(failure_count.get(remote).copied(), Some(1));
assert_eq!(backoff_until.get(remote).copied(), Some(now + 2_000));
}
#[test]
fn active_probe_preserves_a_transport_the_runtime_still_owns() {
assert_eq!(
active_connection_probe_decision(true, true),
ActiveConnectionProbeDecision::Healthy
);
assert_eq!(
active_connection_probe_decision(true, false),
ActiveConnectionProbeDecision::Healthy,
"a successful active probe is authoritative positive liveness evidence"
);
assert_eq!(
active_connection_probe_decision(false, true),
ActiveConnectionProbeDecision::PreserveLiveTransport,
"a diagnostic probe miss must not tear down a live transport before its persistent route settles"
);
assert_eq!(
active_connection_probe_decision(false, false),
ActiveConnectionProbeDecision::Failed
);
}
#[test]
fn proven_independent_route_prevents_fallback_probe_from_demoting_peer() {
assert_eq!(
active_connection_probe_decision_with_independent_route(false, false, true),
ActiveConnectionProbeDecision::Healthy
);
assert_eq!(
active_connection_probe_decision_with_independent_route(false, false, false),
ActiveConnectionProbeDecision::Failed
);
}
#[test]
fn wasm_generation_bound_optional_route_proof_survives_iroh_fallback_probe_miss() {
for transport in ["webrtc", "webrtc-lan", "webrtc-turn", "moq"] {
assert!(wasm_independent_route_proof_is_current(
transport,
&ConnectionHealth::Healthy,
));
assert!(!wasm_independent_route_proof_is_current(
transport,
&ConnectionHealth::Unknown,
));
}
assert!(!wasm_independent_route_proof_is_current(
"iroh-relay",
&ConnectionHealth::Healthy,
));
}
#[test]
fn connected_transport_presents_until_current_remote_admission_is_proven() {
assert!(
!should_present_session_token_on_connected_transport(false, false),
"bare tickets should still short-circuit on a live transport"
);
assert!(
should_auto_present_session_token(true, false),
"every peer that extracted the remote host's token owns its directional presentation",
);
assert!(!should_auto_present_session_token(false, false));
assert!(!should_auto_present_session_token(true, true));
assert!(
!should_present_session_token_on_connected_transport(true, true),
"a current remote proof suppresses duplicate presentation"
);
assert!(
should_present_session_token_on_connected_transport(true, false),
"local trust cannot substitute for remote host approval"
);
let asymmetric = session_admission_presentation_decision(true, true, false, false);
assert!(asymmetric.should_present);
assert!(
asymmetric.request_reciprocal,
"a missing inbound proof must request the peer's reverse presentation"
);
let replacement_without_proofs =
session_admission_presentation_decision(true, false, false, false);
assert!(replacement_without_proofs.should_present);
assert!(
replacement_without_proofs.request_reciprocal,
"a fresh replacement route must request both directional proofs in one pass"
);
let peer_requested = session_admission_presentation_decision(true, true, true, true);
assert!(
!peer_requested.should_present,
"bilateral current proofs retire a stale repair request without another presentation",
);
assert!(!peer_requested.request_reciprocal);
let peer_requested_while_inbound_proof_is_settling =
session_admission_presentation_decision(true, true, false, true);
assert!(peer_requested_while_inbound_proof_is_settling.should_present);
assert!(
!peer_requested_while_inbound_proof_is_settling.request_reciprocal,
"a validated peer request must be satisfied without echoing another request",
);
let settled = session_admission_presentation_decision(true, true, true, false);
assert!(!settled.should_present);
assert!(!settled.request_reciprocal);
let tokenless = session_admission_presentation_decision(false, true, false, true);
assert!(!tokenless.should_present);
assert!(!tokenless.request_reciprocal);
}
#[cfg(not(target_arch = "wasm32"))]
#[test]
fn reciprocal_admission_request_is_owned_and_retired_with_the_session() {
let client = Client::new_with_app_tag(
crate::test_constants::TEST_PROJECT_ID.to_string(),
"test-app".to_string(),
Box::new(|| None),
);
let connection_id = "reciprocal-repair-connection";
client.request_reciprocal_session_admission(connection_id);
assert!(client.has_pending_reciprocal_session_admission_request(connection_id));
client.forget_session_connection(connection_id);
assert!(!client.has_pending_reciprocal_session_admission_request(connection_id));
}
#[tokio::test]
async fn network_change_recovery_is_suppressed_when_another_peer_is_connected() {
let client = Client::new_with_app_tag(
crate::test_constants::TEST_PROJECT_ID.to_string(),
"test-app".to_string(),
Box::new(|| None),
);
client
.connection_manager
.upsert_pending(
"conn-healthy".to_string(),
Some("node-healthy".to_string()),
Some("device-healthy".to_string()),
Some("node-healthy".to_string()),
)
.await;
client
.connection_manager
.set_connected_with_transport(
"conn-healthy",
Some("node-healthy".to_string()),
None,
Some("health-check".to_string()),
)
.await
.expect("healthy record");
client
.connection_manager
.set_device_id("conn-healthy", "device-healthy".to_string())
.await
.expect("healthy device binding");
client
.connection_manager
.upsert_pending(
"conn-failing".to_string(),
Some("node-failing".to_string()),
Some("device-failing".to_string()),
Some("node-failing".to_string()),
)
.await;
client
.connection_manager
.set_failed("conn-failing", Some("timed out connecting".to_string()))
.await;
assert!(
!client
.should_force_network_change_after_connect_failures("device-failing")
.await
);
}
#[tokio::test]
async fn network_change_recovery_remains_enabled_without_other_connected_peers() {
let client = Client::new_with_app_tag(
crate::test_constants::TEST_PROJECT_ID.to_string(),
"test-app".to_string(),
Box::new(|| None),
);
client
.connection_manager
.upsert_pending(
"conn-failing".to_string(),
Some("node-failing".to_string()),
Some("device-failing".to_string()),
Some("node-failing".to_string()),
)
.await;
client
.connection_manager
.set_failed("conn-failing", Some("timed out connecting".to_string()))
.await;
assert!(
client
.should_force_network_change_after_connect_failures("device-failing")
.await
);
}
#[cfg(all(feature = "transport-webrtc", not(target_arch = "wasm32")))]
#[test]
fn native_webrtc_attempt_budget_latches_after_exhaustion() {
assert_eq!(
Client::reserve_native_webrtc_attempt_slot(0),
NativeWebRTCAttemptReservation::Start(1)
);
assert_eq!(
Client::reserve_native_webrtc_attempt_slot(Client::NATIVE_WEBRTC_MAX_UPGRADE_ATTEMPTS),
NativeWebRTCAttemptReservation::JustExhausted(
Client::NATIVE_WEBRTC_MAX_UPGRADE_ATTEMPTS + 1
)
);
assert_eq!(
Client::reserve_native_webrtc_attempt_slot(
Client::NATIVE_WEBRTC_MAX_UPGRADE_ATTEMPTS + 1
),
NativeWebRTCAttemptReservation::AlreadyExhausted
);
}
#[cfg(all(feature = "transport-webrtc", not(target_arch = "wasm32")))]
#[tokio::test]
async fn native_webrtc_start_gate_serializes_concurrent_callers() {
let client = Client::new_with_app_tag(
crate::test_constants::TEST_PROJECT_ID.to_string(),
"test-app".to_string(),
Box::new(|| None),
);
let first_gate = client
.acquire_native_webrtc_start_gate("conn-1", "node-1", false, None)
.await;
let waiter_client = client.clone();
let waiter = tokio::spawn(async move {
let gate = waiter_client
.acquire_native_webrtc_start_gate("conn-1", "node-1", false, None)
.await;
waiter_client
.release_native_webrtc_start_gate("conn-1", gate)
.await;
});
sleep(Duration::from_millis(100)).await;
assert!(
!waiter.is_finished(),
"second caller should wait while the first start gate is held"
);
client
.release_native_webrtc_start_gate("conn-1", first_gate)
.await;
tokio::time::timeout(Duration::from_secs(1), waiter)
.await
.expect("waiting caller should acquire the gate after release")
.expect("waiting task should finish cleanly");
}
#[test]
fn duplicate_kept_existing_close_reason_detection_is_specific() {
assert!(is_duplicate_kept_existing_close_reason(Some(
"ApplicationClosed(0, b\"duplicate-kept-existing\")"
)));
assert!(is_duplicate_kept_existing_close_reason(Some(
"ApplicationClosed(0, b\"duplicate-kept-existing:12345\")"
)));
assert!(is_duplicate_kept_existing_close_reason(Some(
"duplicate-kept-existing:99"
)));
assert!(!is_duplicate_kept_existing_close_reason(Some(
"ApplicationClosed(0, b\"disconnected by user\")"
)));
assert!(!is_duplicate_kept_existing_close_reason(None));
}
#[test]
fn replacement_churn_close_reason_detection_is_specific() {
assert!(is_replacement_churn_close_reason(Some(
"ApplicationClosed(0, b\"duplicate-kept-existing\")"
)));
assert!(is_replacement_churn_close_reason(Some(
"ApplicationClosed(ApplicationClose { error_code: 0, reason: b\"duplicate-accept-superseded\" })"
)));
assert!(is_replacement_churn_close_reason(Some(
"ApplicationClosed(ApplicationClose { error_code: 0, reason: b\"duplicate-dial-superseded\" })"
)));
assert!(is_replacement_churn_close_reason(Some(
"ApplicationClosed(ApplicationClose { error_code: 0, reason: b\"replaced-by-new-inbound\" })"
)));
assert!(is_replacement_churn_close_reason(Some(
"ApplicationClosed(ApplicationClose { error_code: 0, reason: b\"replaced-by-new-outbound\" })"
)));
assert!(is_replacement_churn_close_reason(Some(
"ApplicationClosed(0, b\"zombie-replaced-by-fresh-dial\")"
)));
assert!(!is_replacement_churn_close_reason(Some(
"ApplicationClosed(0, b\"manual disconnect\")"
)));
assert!(!is_replacement_churn_close_reason(Some("LocallyClosed")));
assert!(!is_replacement_churn_close_reason(None));
}
#[test]
fn graceful_disconnect_close_reason_detection_is_specific() {
assert!(is_graceful_disconnect_close_reason(Some(
"ApplicationClosed(0, b\"closed\")"
)));
assert!(is_graceful_disconnect_close_reason(Some(
"ApplicationClosed(1, b\"disconnected by user\")"
)));
assert!(is_graceful_disconnect_close_reason(Some(
"manual disconnect"
)));
assert!(!is_graceful_disconnect_close_reason(Some(
"ApplicationClosed(0, b\"duplicate-kept-existing\")"
)));
assert!(!is_graceful_disconnect_close_reason(Some(
"ApplicationClosed(1, b\"session-token-revoked\")"
)));
assert!(!is_graceful_disconnect_close_reason(Some(
"transport reset by peer"
)));
assert!(!is_graceful_disconnect_close_reason(None));
}
#[test]
fn terminal_disconnect_close_reason_settles_authorization_failures() {
assert!(is_terminal_disconnect_close_reason(Some(
"ApplicationClosed(0, b\"closed\")"
)));
assert!(is_terminal_disconnect_close_reason(Some(
"ApplicationClosed(1, b\"session-token-revoked\")"
)));
assert!(is_terminal_disconnect_close_reason(Some(
"ApplicationClosed(1, b\"session-admission-rejected\")"
)));
assert!(!is_terminal_disconnect_close_reason(Some(
"ApplicationClosed(0, b\"duplicate-kept-existing\")"
)));
assert!(!is_terminal_disconnect_close_reason(Some(
"transport reset by peer"
)));
assert!(!is_terminal_disconnect_close_reason(None));
}
#[test]
fn manual_disconnect_close_reason_detection_is_session_scoped() {
assert!(!is_manual_disconnect_close_reason(Some(
"ApplicationClosed(1, b\"disconnected by user\")"
)));
assert!(is_manual_disconnect_close_reason(Some(
"ApplicationClosed(0, b\"manual disconnect\")"
)));
assert!(!is_manual_disconnect_close_reason(Some(
"ApplicationClosed(0, b\"closed\")"
)));
assert!(!is_manual_disconnect_close_reason(Some(
"ApplicationClosed(1, b\"session-token-revoked\")"
)));
assert!(!is_manual_disconnect_close_reason(None));
}
#[test]
fn device_status_snapshots_prefer_connected_peer_by_device_id() {
let devices = vec![Device {
app_tag: Some("app".to_string()),
device_id: "device-1".to_string(),
user_id: Some("user-1".to_string()),
device_name: "Device 1".to_string(),
platform_type: Some("desktop".to_string()),
capabilities: None,
session_id: None,
node_id: Some("node-1".to_string()),
tag: Some("app".to_string()),
kind: Some("device".to_string()),
metadata: None,
online: true,
ticket: Some("ticket".to_string()),
last_seen_at: None,
expires_at: None,
created_at: None,
updated_at: None,
excluded_peers: vec![],
}];
let peers = vec![
PeerSnapshot {
peer_id: "device-1".to_string(),
device_id: Some("device-1".to_string()),
device_id_hint: None,
node_id: Some("node-1".to_string()),
connection_ids: vec!["conn-stale".to_string()],
active_transport_stable_id: Some(1),
active_transport_generation: 1,
active_route_generation: 0,
active_transport: "iroh".to_string(),
parallel_transport: None,
last_route_change_at_ms: 1,
last_lifecycle_transition_at_ms: 1,
status: ConnectionState::Connecting,
health: ConnectionHealth::Suspect,
transition_count: 1,
connecting_transition_count: 1,
replacement_count: 0,
retire_count: 0,
last_disconnect_reason: None,
last_reconnect_reason: Some("initial".to_string()),
scopes: vec!["session".to_string()],
last_seen_at_ms: 1,
error: None,
},
PeerSnapshot {
peer_id: "device-1".to_string(),
device_id: Some("device-1".to_string()),
device_id_hint: Some("old-device-1".to_string()),
node_id: Some("node-1".to_string()),
connection_ids: vec!["conn-live".to_string()],
active_transport_stable_id: Some(2),
active_transport_generation: 2,
active_route_generation: 0,
active_transport: "webrtc".to_string(),
parallel_transport: Some("iroh".to_string()),
last_route_change_at_ms: 2,
last_lifecycle_transition_at_ms: 2,
status: ConnectionState::Connected,
health: ConnectionHealth::Healthy,
transition_count: 2,
connecting_transition_count: 1,
replacement_count: 1,
retire_count: 0,
last_disconnect_reason: None,
last_reconnect_reason: Some("replacement".to_string()),
scopes: vec!["persistent".to_string()],
last_seen_at_ms: 2,
error: None,
},
];
let snapshots = merge_device_status_snapshots(devices, peers);
let snapshot = snapshots.first().expect("snapshot");
assert_eq!(
snapshot.connection_status,
DeviceConnectionStatus::Connected
);
assert_eq!(snapshot.peer_health, ConnectionHealth::Healthy);
assert_eq!(snapshot.connection_id.as_deref(), Some("conn-live"));
assert_eq!(snapshot.device_id_hint.as_deref(), Some("old-device-1"));
}
#[test]
fn device_status_snapshots_do_not_project_a_retired_node_onto_its_replacement() {
let devices = vec![Device {
app_tag: Some("app".to_string()),
device_id: "durable-device".to_string(),
user_id: Some("user-1".to_string()),
device_name: "Restarted device".to_string(),
platform_type: Some("desktop".to_string()),
capabilities: None,
session_id: None,
node_id: Some("replacement-node".to_string()),
tag: Some("app".to_string()),
kind: Some("device".to_string()),
metadata: None,
online: true,
ticket: Some("replacement-ticket".to_string()),
last_seen_at: None,
expires_at: None,
created_at: None,
updated_at: None,
excluded_peers: vec![],
}];
let peers = vec![PeerSnapshot {
peer_id: "durable-device".to_string(),
device_id: Some("durable-device".to_string()),
device_id_hint: None,
node_id: Some("retired-node".to_string()),
connection_ids: vec!["retired-connection".to_string()],
active_transport_stable_id: Some(17),
active_transport_generation: 4,
active_route_generation: 2,
active_transport: "iroh-lan".to_string(),
parallel_transport: None,
last_route_change_at_ms: 20,
last_lifecycle_transition_at_ms: 20,
status: ConnectionState::Connected,
health: ConnectionHealth::Healthy,
transition_count: 1,
connecting_transition_count: 0,
replacement_count: 1,
retire_count: 0,
last_disconnect_reason: None,
last_reconnect_reason: Some("health-check".to_string()),
scopes: vec!["user-device".to_string()],
last_seen_at_ms: 20,
error: None,
}];
let snapshots = merge_device_status_snapshots(devices, peers);
let snapshot = snapshots.first().expect("snapshot");
assert_eq!(snapshot.device.node_id.as_deref(), Some("replacement-node"));
assert_eq!(snapshot.connection_status, DeviceConnectionStatus::Online);
assert!(!snapshot.settled_ready);
assert_eq!(snapshot.readiness_state, ReadinessState::Connecting);
assert!(snapshot.connection_id.is_none());
assert!(snapshot.active_transport_stable_id.is_none());
}
#[test]
fn device_status_snapshots_prefer_routable_peer_when_aliases_overlap() {
let devices = vec![Device {
app_tag: Some("app".to_string()),
device_id: "device-routable".to_string(),
user_id: Some("user-1".to_string()),
device_name: "Device Routable".to_string(),
platform_type: Some("web".to_string()),
capabilities: None,
session_id: None,
node_id: Some("node-routable".to_string()),
tag: Some("app".to_string()),
kind: Some("device".to_string()),
metadata: None,
online: true,
ticket: Some("ticket".to_string()),
last_seen_at: None,
expires_at: None,
created_at: None,
updated_at: None,
excluded_peers: vec![],
}];
let peers = vec![
PeerSnapshot {
peer_id: "device-routable".to_string(),
device_id: Some("device-routable".to_string()),
device_id_hint: None,
node_id: Some("node-routable".to_string()),
connection_ids: vec!["conn-stale-connected".to_string()],
active_transport_stable_id: Some(7),
active_transport_generation: 1,
active_route_generation: 0,
active_transport: "iroh".to_string(),
parallel_transport: None,
last_route_change_at_ms: 10,
last_lifecycle_transition_at_ms: 10,
status: ConnectionState::Connected,
health: ConnectionHealth::Stale,
transition_count: 1,
connecting_transition_count: 1,
replacement_count: 0,
retire_count: 0,
last_disconnect_reason: None,
last_reconnect_reason: Some("health-check".to_string()),
scopes: vec![],
last_seen_at_ms: 10,
error: None,
},
PeerSnapshot {
peer_id: "device-routable".to_string(),
device_id: Some("device-routable".to_string()),
device_id_hint: None,
node_id: Some("node-routable".to_string()),
connection_ids: vec!["conn-routable".to_string()],
active_transport_stable_id: Some(8),
active_transport_generation: 2,
active_route_generation: 0,
active_transport: "webrtc".to_string(),
parallel_transport: Some("iroh".to_string()),
last_route_change_at_ms: 20,
last_lifecycle_transition_at_ms: 20,
status: ConnectionState::Connected,
health: ConnectionHealth::Healthy,
transition_count: 2,
connecting_transition_count: 1,
replacement_count: 1,
retire_count: 0,
last_disconnect_reason: None,
last_reconnect_reason: Some("replacement".to_string()),
scopes: vec!["persistent".to_string()],
last_seen_at_ms: 20,
error: None,
},
];
let snapshots = merge_device_status_snapshots(devices, peers);
let snapshot = snapshots.first().expect("snapshot");
assert_eq!(
snapshot.connection_status,
DeviceConnectionStatus::Connected
);
assert_eq!(snapshot.settled_ready, true);
assert_eq!(snapshot.peer_health, ConnectionHealth::Healthy);
assert_eq!(snapshot.connection_id.as_deref(), Some("conn-routable"));
assert_eq!(snapshot.active_transport_stable_id, Some(8));
assert_eq!(snapshot.active_transport, "webrtc");
assert_eq!(snapshot.parallel_transport.as_deref(), Some("iroh"));
}
#[test]
fn device_status_snapshots_choose_best_peer_across_device_and_node_aliases() {
let devices = vec![Device {
app_tag: Some("app".to_string()),
device_id: "device-cross-alias".to_string(),
user_id: Some("user-1".to_string()),
device_name: "Device Cross Alias".to_string(),
platform_type: Some("web".to_string()),
capabilities: None,
session_id: None,
node_id: Some("node-cross-alias".to_string()),
tag: Some("app".to_string()),
kind: Some("device".to_string()),
metadata: None,
online: true,
ticket: Some("ticket".to_string()),
last_seen_at: None,
expires_at: None,
created_at: None,
updated_at: None,
excluded_peers: vec![],
}];
let peers = vec![
PeerSnapshot {
peer_id: "device-cross-alias".to_string(),
device_id: Some("device-cross-alias".to_string()),
device_id_hint: None,
node_id: Some("node-cross-alias".to_string()),
connection_ids: vec!["conn-cross-alias-stale".to_string()],
active_transport_stable_id: None,
active_transport_generation: 1,
active_route_generation: 0,
active_transport: "iroh-relay".to_string(),
parallel_transport: None,
last_route_change_at_ms: 10,
last_lifecycle_transition_at_ms: 10,
status: ConnectionState::Connected,
health: ConnectionHealth::Unknown,
transition_count: 1,
connecting_transition_count: 1,
replacement_count: 0,
retire_count: 0,
last_disconnect_reason: None,
last_reconnect_reason: Some("transient-disconnect".to_string()),
scopes: vec![],
last_seen_at_ms: 10,
error: None,
},
PeerSnapshot {
peer_id: "conn-cross-alias-live".to_string(),
device_id: None,
device_id_hint: None,
node_id: Some("node-cross-alias".to_string()),
connection_ids: vec!["conn-cross-alias-live".to_string()],
active_transport_stable_id: Some(8),
active_transport_generation: 2,
active_route_generation: 0,
active_transport: "webrtc".to_string(),
parallel_transport: Some("iroh-relay".to_string()),
last_route_change_at_ms: 20,
last_lifecycle_transition_at_ms: 20,
status: ConnectionState::Connected,
health: ConnectionHealth::Healthy,
transition_count: 2,
connecting_transition_count: 1,
replacement_count: 1,
retire_count: 0,
last_disconnect_reason: None,
last_reconnect_reason: Some("replacement".to_string()),
scopes: vec![],
last_seen_at_ms: 20,
error: None,
},
];
let snapshots = merge_device_status_snapshots(devices, peers);
let snapshot = snapshots.first().expect("snapshot");
assert_eq!(
snapshot.connection_status,
DeviceConnectionStatus::Connected
);
assert!(snapshot.settled_ready);
assert_eq!(
snapshot.connection_id.as_deref(),
Some("conn-cross-alias-live")
);
assert_eq!(snapshot.active_transport, "webrtc");
}
#[test]
fn device_status_merge_prefers_connected_with_live_transport_over_newer_failed() {
let devices = vec![Device {
app_tag: Some("app".to_string()),
device_id: "device-dup-fail".to_string(),
user_id: Some("user-1".to_string()),
device_name: "Dup fail".to_string(),
platform_type: Some("web".to_string()),
capabilities: None,
session_id: None,
node_id: Some("node-dup-fail".to_string()),
tag: Some("app".to_string()),
kind: Some("device".to_string()),
metadata: None,
online: true,
ticket: Some("ticket".to_string()),
last_seen_at: None,
expires_at: None,
created_at: None,
updated_at: None,
excluded_peers: vec![],
}];
let connected_with_transport = PeerSnapshot {
peer_id: "peer-by-connection-id-live".to_string(),
device_id: Some("device-dup-fail".to_string()),
device_id_hint: None,
node_id: Some("node-dup-fail".to_string()),
connection_ids: vec!["live-conn".to_string()],
active_transport_stable_id: Some(1),
active_transport_generation: 1,
active_route_generation: 0,
active_transport: "iroh".to_string(),
parallel_transport: None,
last_route_change_at_ms: 1000,
last_lifecycle_transition_at_ms: 1000,
status: ConnectionState::Connected,
health: ConnectionHealth::Healthy,
transition_count: 1,
connecting_transition_count: 0,
replacement_count: 0,
retire_count: 0,
last_disconnect_reason: None,
last_reconnect_reason: None,
scopes: vec![],
last_seen_at_ms: 1000,
error: None,
};
let failed_newer = PeerSnapshot {
peer_id: "device-dup-fail".to_string(),
device_id: Some("device-dup-fail".to_string()),
device_id_hint: None,
node_id: Some("node-dup-fail".to_string()),
connection_ids: vec!["dial-conn-failed".to_string()],
active_transport_stable_id: None,
active_transport_generation: 0,
active_route_generation: 0,
active_transport: "iroh".to_string(),
parallel_transport: None,
last_route_change_at_ms: 2000,
last_lifecycle_transition_at_ms: 2000,
status: ConnectionState::Failed,
health: ConnectionHealth::Stale,
transition_count: 1,
connecting_transition_count: 1,
replacement_count: 0,
retire_count: 0,
last_disconnect_reason: None,
last_reconnect_reason: None,
scopes: vec![],
last_seen_at_ms: 2000,
error: Some("timeout".to_string()),
};
for peers in [
vec![connected_with_transport.clone(), failed_newer.clone()],
vec![failed_newer.clone(), connected_with_transport.clone()],
] {
let snapshots = merge_device_status_snapshots(devices.clone(), peers);
let snapshot = snapshots.first().expect("snapshot");
assert_eq!(
snapshot.connection_status,
DeviceConnectionStatus::Connected,
"Connected snapshot with live transport must beat newer Failed (merge order invariance)",
);
assert!(
snapshot.settled_ready,
"Live-transport Connected snapshot must report settled_ready",
);
}
}
#[test]
fn independent_transport_without_base_stable_id_is_routable() {
let devices = vec![Device {
app_tag: Some("app".to_string()),
device_id: "device-webrtc".to_string(),
user_id: Some("user-1".to_string()),
device_name: "WebRTC peer".to_string(),
platform_type: Some("web".to_string()),
capabilities: None,
session_id: None,
node_id: Some("node-webrtc".to_string()),
tag: Some("app".to_string()),
kind: Some("device".to_string()),
metadata: None,
online: true,
ticket: Some("ticket".to_string()),
last_seen_at: None,
expires_at: None,
created_at: None,
updated_at: None,
excluded_peers: vec![],
}];
let peer = PeerSnapshot {
peer_id: "device-webrtc".to_string(),
device_id: Some("device-webrtc".to_string()),
device_id_hint: None,
node_id: Some("node-webrtc".to_string()),
connection_ids: vec!["webrtc-conn".to_string()],
active_transport_stable_id: None,
active_transport_generation: 4,
active_route_generation: 0,
active_transport: "webrtc".to_string(),
parallel_transport: Some("iroh".to_string()),
last_route_change_at_ms: 1000,
last_lifecycle_transition_at_ms: 1000,
status: ConnectionState::Connected,
health: ConnectionHealth::Healthy,
transition_count: 2,
connecting_transition_count: 0,
replacement_count: 1,
retire_count: 0,
last_disconnect_reason: None,
last_reconnect_reason: Some("transient-disconnect".to_string()),
scopes: vec![],
last_seen_at_ms: 1000,
error: None,
};
let snapshots = merge_device_status_snapshots(devices, vec![peer]);
let snapshot = snapshots.first().expect("snapshot");
assert_eq!(
snapshot.connection_status,
DeviceConnectionStatus::Connected
);
assert_eq!(snapshot.readiness_state, ReadinessState::Routable);
assert!(
snapshot.settled_ready,
"active independent WebRTC transport must be routable while the base Iroh stable id is absent",
);
assert_eq!(snapshot.active_transport, "webrtc");
assert_eq!(snapshot.parallel_transport.as_deref(), Some("iroh"));
assert_eq!(snapshot.active_transport_stable_id, None);
}
#[test]
fn ghost_connected_without_transport_yields_to_newer_failed() {
let devices = vec![Device {
app_tag: Some("app".to_string()),
device_id: "device-ghost".to_string(),
user_id: Some("user-1".to_string()),
device_name: "Ghost".to_string(),
platform_type: Some("web".to_string()),
capabilities: None,
session_id: None,
node_id: Some("node-ghost".to_string()),
tag: Some("app".to_string()),
kind: Some("device".to_string()),
metadata: None,
online: true,
ticket: Some("ticket".to_string()),
last_seen_at: None,
expires_at: None,
created_at: None,
updated_at: None,
excluded_peers: vec![],
}];
let connected_ghost = PeerSnapshot {
peer_id: "peer-by-connection-id-ghost".to_string(),
device_id: Some("device-ghost".to_string()),
device_id_hint: None,
node_id: Some("node-ghost".to_string()),
connection_ids: vec!["ghost-conn".to_string()],
active_transport_stable_id: None,
active_transport_generation: 0,
active_route_generation: 0,
active_transport: "iroh".to_string(),
parallel_transport: None,
last_route_change_at_ms: 1000,
last_lifecycle_transition_at_ms: 1000,
status: ConnectionState::Connected,
health: ConnectionHealth::Healthy,
transition_count: 1,
connecting_transition_count: 0,
replacement_count: 0,
retire_count: 0,
last_disconnect_reason: None,
last_reconnect_reason: None,
scopes: vec![],
last_seen_at_ms: 1000,
error: None,
};
let failed_newer = PeerSnapshot {
peer_id: "device-ghost".to_string(),
device_id: Some("device-ghost".to_string()),
device_id_hint: None,
node_id: Some("node-ghost".to_string()),
connection_ids: vec!["dial-conn-failed".to_string()],
active_transport_stable_id: None,
active_transport_generation: 0,
active_route_generation: 0,
active_transport: "iroh".to_string(),
parallel_transport: None,
last_route_change_at_ms: 2000,
last_lifecycle_transition_at_ms: 2000,
status: ConnectionState::Failed,
health: ConnectionHealth::Stale,
transition_count: 1,
connecting_transition_count: 1,
replacement_count: 0,
retire_count: 0,
last_disconnect_reason: None,
last_reconnect_reason: None,
scopes: vec![],
last_seen_at_ms: 2000,
error: Some("timeout".to_string()),
};
for peers in [
vec![connected_ghost.clone(), failed_newer.clone()],
vec![failed_newer.clone(), connected_ghost.clone()],
] {
let snapshots = merge_device_status_snapshots(devices.clone(), peers);
let snapshot = snapshots.first().expect("snapshot");
assert_eq!(
snapshot.connection_status,
DeviceConnectionStatus::Failed,
"Connected snapshot without live transport must yield to newer Failed",
);
}
}
#[test]
fn device_status_snapshots_report_online_without_peer() {
let devices = vec![Device {
app_tag: Some("app".to_string()),
device_id: "device-2".to_string(),
user_id: Some("user-1".to_string()),
device_name: "Device 2".to_string(),
platform_type: Some("web".to_string()),
capabilities: None,
session_id: None,
node_id: Some("node-2".to_string()),
tag: Some("app".to_string()),
kind: Some("device".to_string()),
metadata: None,
online: true,
ticket: Some("ticket".to_string()),
last_seen_at: None,
expires_at: None,
created_at: None,
updated_at: None,
excluded_peers: vec![],
}];
let snapshots = merge_device_status_snapshots(devices, Vec::new());
let snapshot = snapshots.first().expect("snapshot");
assert_eq!(snapshot.connection_status, DeviceConnectionStatus::Online);
assert_eq!(snapshot.presence_status, DevicePresenceStatus::Online);
assert!(snapshot.connectable);
assert_eq!(snapshot.peer_health, ConnectionHealth::Healthy);
assert!(snapshot.connection_id.is_none());
}
#[test]
fn device_status_snapshots_report_stale_online_roster_entries_as_idle() {
let now = crate::firebase::now_millis_u64().min(i64::MAX as u64) as i64;
let devices = vec![Device {
app_tag: Some("app".to_string()),
device_id: "device-idle".to_string(),
user_id: Some("user-1".to_string()),
device_name: "Idle Device".to_string(),
platform_type: Some("desktop".to_string()),
capabilities: None,
session_id: None,
node_id: Some("node-idle".to_string()),
tag: Some("app".to_string()),
kind: Some("device".to_string()),
metadata: None,
online: true,
ticket: Some("ticket".to_string()),
last_seen_at: None,
expires_at: None,
created_at: None,
updated_at: Some(serde_json::Value::from(now.saturating_sub(
crate::firebase::device_registry::DEVICE_STALE_HEARTBEAT_MS + 1,
))),
excluded_peers: vec![],
}];
let snapshots = merge_device_status_snapshots(devices, Vec::new());
let snapshot = snapshots.first().expect("snapshot");
assert_eq!(snapshot.connection_status, DeviceConnectionStatus::Online);
assert_eq!(snapshot.presence_status, DevicePresenceStatus::Idle);
assert!(!snapshot.connectable);
assert!(snapshot.presence_updated_at.is_some());
}
#[test]
fn device_status_snapshots_preserve_offline_device_without_peer() {
let devices = vec![Device {
app_tag: Some("app".to_string()),
device_id: "device-offline".to_string(),
user_id: Some("user-1".to_string()),
device_name: "Offline Device".to_string(),
platform_type: Some("desktop".to_string()),
capabilities: None,
session_id: None,
node_id: Some("node-offline".to_string()),
tag: Some("app".to_string()),
kind: Some("device".to_string()),
metadata: None,
online: false,
ticket: None,
last_seen_at: None,
expires_at: None,
created_at: None,
updated_at: None,
excluded_peers: vec![],
}];
let snapshots = merge_device_status_snapshots(devices, Vec::new());
let snapshot = snapshots.first().expect("snapshot");
assert_eq!(
snapshot.connection_status,
DeviceConnectionStatus::Disconnected
);
assert_eq!(snapshot.presence_status, DevicePresenceStatus::Offline);
assert!(!snapshot.connectable);
assert_eq!(snapshot.readiness_reason, "device-offline");
assert!(snapshot.connection_id.is_none());
}
#[tokio::test]
async fn device_status_snapshots_report_connected_peer_with_hint_and_scope() {
let client = Client::new_with_app_tag(
crate::test_constants::TEST_PROJECT_ID.to_string(),
"test-app".to_string(),
Box::new(|| None),
);
client
.connection_manager
.upsert_pending(
"conn-1".to_string(),
Some("node-1".to_string()),
Some("device-old-1".to_string()),
Some("node-1".to_string()),
)
.await;
client.connection_manager.set_connecting("conn-1").await;
client
.connection_manager
.set_device_id("conn-1", "device-1".to_string())
.await;
client
.connection_manager
.set_connected("conn-1", Some("node-1".to_string()))
.await;
client
.connection_manager
.set_health("conn-1", ConnectionHealth::Healthy)
.await;
client
.connection_manager
.add_scope("conn-1", "persistent")
.await;
let devices = vec![Device {
app_tag: Some("test-app".to_string()),
device_id: "device-1".to_string(),
user_id: Some("user-1".to_string()),
device_name: "Device 1".to_string(),
platform_type: Some("desktop".to_string()),
capabilities: None,
session_id: None,
node_id: Some("node-1".to_string()),
tag: Some("test-app".to_string()),
kind: Some("device".to_string()),
metadata: None,
online: true,
ticket: Some("ticket".to_string()),
last_seen_at: None,
expires_at: None,
created_at: None,
updated_at: None,
excluded_peers: vec![],
}];
let snapshots = merge_device_status_snapshots(
devices,
client.connection_manager.list_peer_snapshots().await,
);
let snapshot = snapshots.first().expect("snapshot");
assert_eq!(
snapshot.connection_status,
DeviceConnectionStatus::Connected
);
assert_eq!(snapshot.peer_health, ConnectionHealth::Healthy);
assert_eq!(snapshot.connection_id.as_deref(), Some("conn-1"));
assert_eq!(snapshot.device_id_hint.as_deref(), Some("device-old-1"));
assert_eq!(snapshot.scopes, vec!["persistent".to_string()]);
}
#[test]
fn auto_connect_excluded_set_and_cleared() {
let client = Client::new_with_app_tag(
crate::test_constants::TEST_PROJECT_ID.to_string(),
"test-app".to_string(),
Box::new(|| None),
);
assert!(!client.is_auto_connect_excluded("device-1"));
client.set_auto_connect_excluded("device-1", true);
assert!(client.is_auto_connect_excluded("device-1"));
client.set_auto_connect_excluded("device-1", false);
assert!(!client.is_auto_connect_excluded("device-1"));
}
#[test]
fn auto_connect_excluded_is_case_insensitive() {
let client = Client::new_with_app_tag(
crate::test_constants::TEST_PROJECT_ID.to_string(),
"test-app".to_string(),
Box::new(|| None),
);
client.set_auto_connect_excluded("Device-ABC", true);
assert!(client.is_auto_connect_excluded("device-abc"));
assert!(client.is_auto_connect_excluded("DEVICE-ABC"));
assert!(client.is_auto_connect_excluded("Device-ABC"));
}
#[test]
fn auto_connect_excluded_does_not_affect_other_devices() {
let client = Client::new_with_app_tag(
crate::test_constants::TEST_PROJECT_ID.to_string(),
"test-app".to_string(),
Box::new(|| None),
);
client.set_auto_connect_excluded("device-1", true);
assert!(client.is_auto_connect_excluded("device-1"));
assert!(!client.is_auto_connect_excluded("device-2"));
assert!(!client.is_auto_connect_excluded("device-3"));
}
#[test]
fn auto_connect_excluded_empty_device_id_is_ignored() {
let client = Client::new_with_app_tag(
crate::test_constants::TEST_PROJECT_ID.to_string(),
"test-app".to_string(),
Box::new(|| None),
);
client.set_auto_connect_excluded("", true);
client.set_auto_connect_excluded(" ", true);
assert!(!client.is_auto_connect_excluded(""));
assert!(!client.is_auto_connect_excluded(" "));
}
#[test]
fn auto_connect_excluded_multiple_devices_independent() {
let client = Client::new_with_app_tag(
crate::test_constants::TEST_PROJECT_ID.to_string(),
"test-app".to_string(),
Box::new(|| None),
);
client.set_auto_connect_excluded("device-A", true);
client.set_auto_connect_excluded("device-B", true);
assert!(client.is_auto_connect_excluded("device-a")); assert!(client.is_auto_connect_excluded("device-b"));
client.set_auto_connect_excluded("device-A", false);
assert!(!client.is_auto_connect_excluded("device-a"));
assert!(client.is_auto_connect_excluded("device-b")); }
#[test]
fn auto_connect_excluded_peer_tracks_node_alias_without_publishing_alias() {
let client = Client::new_with_app_tag(
crate::test_constants::TEST_PROJECT_ID.to_string(),
"test-app".to_string(),
Box::new(|| None),
);
client.set_auto_connect_excluded_peer("Device-A", Some("Node-XYZ"), true);
assert!(client.is_auto_connect_excluded("device-a"));
assert!(client.is_auto_connect_peer_excluded("device-a", Some("node-xyz")));
assert!(client.is_auto_connect_peer_excluded("other-device", Some("node-xyz")));
assert!(!client.is_auto_connect_excluded("node-xyz"));
assert_eq!(client.current_excluded_peers_snapshot(), vec!["device-a"]);
client.set_auto_connect_excluded("Device-A", false);
assert!(!client.is_auto_connect_excluded("device-a"));
assert!(!client.is_auto_connect_peer_excluded("device-a", Some("node-xyz")));
assert!(!client.is_auto_connect_peer_excluded("other-device", Some("node-xyz")));
assert!(client.current_excluded_peers_snapshot().is_empty());
}
#[test]
fn authenticated_peer_reconnect_clears_only_peer_requested_exclusion() {
let client = Client::new_with_app_tag(
crate::test_constants::TEST_PROJECT_ID.to_string(),
"test-app".to_string(),
Box::new(|| None),
);
client.set_peer_requested_auto_connect_excluded("device-peer", Some("node-peer"));
assert!(client.is_auto_connect_peer_excluded("device-peer", Some("node-peer")));
assert!(client.resume_peer_requested_auto_connect_if_authenticated("device-peer"));
assert!(!client.is_auto_connect_peer_excluded("device-peer", Some("node-peer")));
client.set_auto_connect_excluded_peer("device-local", Some("node-local"), true);
assert!(!client.resume_peer_requested_auto_connect_if_authenticated("device-local"));
assert!(client.is_auto_connect_peer_excluded("device-local", Some("node-local")));
}
#[test]
fn crossed_manual_disconnect_keeps_local_exclusion_authoritative() {
let client = Client::new_with_app_tag(
crate::test_constants::TEST_PROJECT_ID.to_string(),
"test-app".to_string(),
Box::new(|| None),
);
client.set_auto_connect_excluded_peer("device-peer", Some("node-peer"), true);
client.set_peer_requested_auto_connect_excluded("device-peer", Some("node-peer"));
assert!(!client.resume_peer_requested_auto_connect_if_authenticated("device-peer"));
assert!(client.is_auto_connect_peer_excluded("device-peer", Some("node-peer")));
client.set_auto_connect_excluded("device-peer", false);
client.set_peer_requested_auto_connect_excluded("device-peer", Some("node-peer"));
client.set_auto_connect_excluded_peer("device-peer", Some("node-peer"), true);
assert!(!client.resume_peer_requested_auto_connect_if_authenticated("device-peer"));
assert!(client.is_auto_connect_peer_excluded("device-peer", Some("node-peer")));
}
fn make_test_device(
device_id: &str,
node_id: &str,
ticket_str: &str,
) -> crate::signaling::Device {
make_test_device_with_excluded(device_id, node_id, ticket_str, vec![])
}
fn make_test_device_with_excluded(
device_id: &str,
node_id: &str,
ticket_str: &str,
excluded_peers: Vec<String>,
) -> crate::signaling::Device {
crate::signaling::Device {
app_tag: None,
device_id: device_id.to_string(),
user_id: None,
device_name: "Test Device".to_string(),
platform_type: None,
capabilities: None,
session_id: None,
node_id: Some(node_id.to_string()),
tag: None,
kind: None,
metadata: None,
online: true,
ticket: Some(ticket_str.to_string()),
last_seen_at: None,
expires_at: None,
created_at: None,
updated_at: None,
excluded_peers,
}
}
fn make_loop_state() -> (
std::collections::HashMap<String, i64>,
std::collections::HashMap<String, u8>,
std::collections::HashMap<String, i64>,
std::collections::HashMap<String, i64>,
std::collections::HashMap<String, u8>,
std::collections::HashMap<String, i64>,
std::collections::HashMap<String, i64>,
std::collections::HashMap<String, bool>,
std::collections::HashMap<String, u8>,
i64,
i64,
std::collections::HashMap<String, String>,
) {
(
std::collections::HashMap::new(), std::collections::HashMap::new(), std::collections::HashMap::new(), std::collections::HashMap::new(), std::collections::HashMap::new(), std::collections::HashMap::new(), std::collections::HashMap::new(), std::collections::HashMap::new(), std::collections::HashMap::new(), 0i64, 0i64, std::collections::HashMap::new(), )
}
async fn run_test_auto_connect_once(
client: &std::sync::Arc<Client>,
device: crate::signaling::Device,
local_device_id: &str,
) {
let (
mut last_attempt_at,
mut failure_count,
mut failure_backoff_until,
mut non_initiator_wait_started_at,
mut non_initiator_escalation_count,
mut skip_log_at,
mut connected_health_checked_at,
mut connected_health_state,
mut connected_health_failures,
mut last_presence_republish_at_ms,
mut last_network_change_at_ms,
mut last_known_node_id,
) = make_loop_state();
client
.try_auto_connect_device(
"user-1",
device,
local_device_id,
&mut last_attempt_at,
&mut failure_count,
&mut failure_backoff_until,
500,
&mut non_initiator_wait_started_at,
&mut non_initiator_escalation_count,
0,
&mut skip_log_at,
&mut connected_health_checked_at,
&mut connected_health_state,
&mut connected_health_failures,
5_000,
2,
&mut last_presence_republish_at_ms,
60_000,
&mut last_network_change_at_ms,
30_000,
2,
&mut last_known_node_id,
)
.await;
}
#[tokio::test]
async fn crossed_authorized_dials_settle_quickly_and_do_not_drop_after_arbitration() {
let peer_a = std::sync::Arc::new(Client::new_with_app_tag(
crate::test_constants::TEST_PROJECT_ID.to_string(),
"crossed-authorized-a".to_string(),
Box::new(|| None),
));
let peer_b = std::sync::Arc::new(Client::new_with_app_tag(
crate::test_constants::TEST_PROJECT_ID.to_string(),
"crossed-authorized-b".to_string(),
Box::new(|| None),
));
let endpoint_a = Endpoint::builder(iroh::endpoint::presets::N0)
.alpns(vec![crate::native_node::PlutoniumProtocol::ALPN.to_vec()])
.relay_mode(RelayMode::Disabled)
.bind_addr(
"127.0.0.1:0"
.parse::<std::net::SocketAddr>()
.expect("peer a bind addr"),
)
.expect("peer a endpoint builder")
.bind()
.await
.expect("peer a endpoint");
let endpoint_b = Endpoint::builder(iroh::endpoint::presets::N0)
.alpns(vec![crate::native_node::PlutoniumProtocol::ALPN.to_vec()])
.relay_mode(RelayMode::Disabled)
.bind_addr(
"127.0.0.1:0"
.parse::<std::net::SocketAddr>()
.expect("peer b bind addr"),
)
.expect("peer b endpoint builder")
.bind()
.await
.expect("peer b endpoint");
peer_a.adopt_endpoint(endpoint_a).await;
peer_b.adopt_endpoint(endpoint_b).await;
peer_a.ensure_default_admission_gate("__startup");
peer_b.ensure_default_admission_gate("__startup");
let endpoint_a = peer_a.get_endpoint().await.expect("peer a endpoint handle");
let endpoint_b = peer_b.get_endpoint().await.expect("peer b endpoint handle");
let endpoint_a_id = endpoint_a.id();
let endpoint_b_id = endpoint_b.id();
let endpoint_a_addr = wait_for_endpoint_addr(&endpoint_a, 5_000)
.await
.expect("peer a endpoint addr");
let endpoint_b_addr = wait_for_endpoint_addr(&endpoint_b, 5_000)
.await
.expect("peer b endpoint addr");
let token_a = "crossed-authorized-token-a";
let token_b = "crossed-authorized-token-b";
peer_a.register_session_token(token_a.to_string(), "user-device".to_string(), 0);
peer_b.register_session_token(token_b.to_string(), "user-device".to_string(), 0);
let ticket_a = crate::session_token::build_compound_ticket(
peer_a
.endpoint_ticket()
.await
.expect("peer a ticket")
.as_str(),
token_a,
"user-device",
0,
);
let ticket_b = crate::session_token::build_compound_ticket(
peer_b
.endpoint_ticket()
.await
.expect("peer b ticket")
.as_str(),
token_b,
"user-device",
0,
);
let (dial_a, dial_b) = tokio::join!(
peer_a.ensure_connected_addr(endpoint_b_id, endpoint_b_addr),
peer_b.ensure_connected_addr(endpoint_a_id, endpoint_a_addr),
);
dial_a.expect("peer a crossed dial");
dial_b.expect("peer b crossed dial");
let device_a = make_test_device("crossed-device-a", &endpoint_a_id.to_string(), &ticket_a);
let device_b = make_test_device("crossed-device-b", &endpoint_b_id.to_string(), &ticket_b);
let started_at = std::time::Instant::now();
tokio::time::timeout(Duration::from_secs(3), async {
tokio::join!(
run_test_auto_connect_once(&peer_a, device_b, "crossed-device-a"),
run_test_auto_connect_once(&peer_b, device_a, "crossed-device-b"),
);
let (settled_a, settled_b) = tokio::join!(
peer_a.wait_for_settled_peer("crossed-device-b", Some(2_000)),
peer_b.wait_for_settled_peer("crossed-device-a", Some(2_000)),
);
assert!(settled_a.expect("peer a settled snapshot").settled_ready);
assert!(settled_b.expect("peer b settled snapshot").settled_ready);
})
.await
.expect("crossed authorized peers must settle within three seconds");
assert!(started_at.elapsed() < Duration::from_secs(3));
let before_a = peer_a
.peer_session("crossed-device-b")
.await
.expect("peer a session before quiescence");
let before_b = peer_b
.peer_session("crossed-device-a")
.await
.expect("peer b session before quiescence");
sleep(Duration::from_millis(750)).await;
let after_a = peer_a
.peer_session("crossed-device-b")
.await
.expect("peer a session after quiescence");
let after_b = peer_b
.peer_session("crossed-device-a")
.await
.expect("peer b session after quiescence");
assert!(after_a.settled_ready, "peer a dropped after settlement");
assert!(after_b.settled_ready, "peer b dropped after settlement");
assert_eq!(after_a.transport_generation, before_a.transport_generation);
assert_eq!(after_b.transport_generation, before_b.transport_generation);
assert_eq!(
after_a.active_transport_stable_id,
before_a.active_transport_stable_id
);
assert_eq!(
after_b.active_transport_stable_id,
before_b.active_transport_stable_id
);
}
#[tokio::test]
async fn session_token_presentation_with_local_claim_is_idempotent_after_transport_proof_migration(
) {
let dialer = Client::new_with_app_tag(
crate::test_constants::TEST_PROJECT_ID.to_string(),
"proof-race-dialer".to_string(),
Box::new(|| None),
);
let host = Client::new_with_app_tag(
crate::test_constants::TEST_PROJECT_ID.to_string(),
"proof-race-host".to_string(),
Box::new(|| None),
);
let host_endpoint = Endpoint::builder(iroh::endpoint::presets::N0)
.alpns(vec![crate::native_node::PlutoniumProtocol::ALPN.to_vec()])
.relay_mode(RelayMode::Disabled)
.bind_addr(
"127.0.0.1:0"
.parse::<std::net::SocketAddr>()
.expect("host bind addr"),
)
.expect("host endpoint builder")
.bind()
.await
.expect("host endpoint");
let host_endpoint_id = host_endpoint.id();
let host_addr = wait_for_endpoint_addr(&host_endpoint, 5_000)
.await
.expect("host endpoint addr");
host.adopt_endpoint(host_endpoint).await;
let dialer_endpoint = Endpoint::builder(iroh::endpoint::presets::N0)
.alpns(vec![crate::native_node::PlutoniumProtocol::ALPN.to_vec()])
.relay_mode(RelayMode::Disabled)
.bind_addr(
"127.0.0.1:0"
.parse::<std::net::SocketAddr>()
.expect("dialer bind addr"),
)
.expect("dialer endpoint builder")
.bind()
.await
.expect("dialer endpoint");
dialer.adopt_endpoint(dialer_endpoint).await;
dialer
.ensure_connected_addr(host_endpoint_id, host_addr)
.await
.expect("connected transport");
let connection_id = "proof-race-connection";
let token = "proof-race-token";
let stable_id = dialer
.get_connection(host_endpoint_id)
.await
.expect("current connection")
.stable_id() as u64;
dialer
.remote_session_admission_proofs
.write()
.expect("proof registry")
.insert(
connection_id.to_string(),
RemoteSessionAdmissionProof {
transport_stable_id: stable_id,
token_fingerprint: crate::session_token::token_fingerprint(token),
approval_scope: "user-device".to_string(),
},
);
let scope = tokio::time::timeout(
Duration::from_millis(100),
dialer.present_and_accept_session_token_with_local_claim(
host_endpoint_id,
connection_id,
token,
None,
Some("remote-device".to_string()),
Some("dialer-device".to_string()),
),
)
.await
.expect("current proof must avoid opening another admission stream")
.expect("proof-backed presentation");
assert_eq!(scope, "user-device");
}
#[tokio::test]
async fn inline_reciprocal_ack_commits_only_the_current_transport_generation() {
let client = Client::new_with_app_tag(
crate::test_constants::TEST_PROJECT_ID.to_string(),
"inline-reciprocal-ack".to_string(),
Box::new(|| None),
);
let remote = Client::new_with_app_tag(
crate::test_constants::TEST_PROJECT_ID.to_string(),
"inline-reciprocal-ack-remote".to_string(),
Box::new(|| None),
);
let remote_endpoint = Endpoint::builder(iroh::endpoint::presets::N0)
.alpns(vec![crate::native_node::PlutoniumProtocol::ALPN.to_vec()])
.relay_mode(RelayMode::Disabled)
.bind_addr(
"127.0.0.1:0"
.parse::<std::net::SocketAddr>()
.expect("remote bind addr"),
)
.expect("remote endpoint builder")
.bind()
.await
.expect("remote endpoint");
let remote_endpoint_id = remote_endpoint.id();
let remote_addr = wait_for_endpoint_addr(&remote_endpoint, 5_000)
.await
.expect("remote endpoint addr");
remote.adopt_endpoint(remote_endpoint).await;
let local_endpoint = Endpoint::builder(iroh::endpoint::presets::N0)
.alpns(vec![crate::native_node::PlutoniumProtocol::ALPN.to_vec()])
.relay_mode(RelayMode::Disabled)
.bind_addr(
"127.0.0.1:0"
.parse::<std::net::SocketAddr>()
.expect("local bind addr"),
)
.expect("local endpoint builder")
.bind()
.await
.expect("local endpoint");
client.adopt_endpoint(local_endpoint).await;
client
.ensure_connected_addr(remote_endpoint_id, remote_addr)
.await
.expect("connected transport");
let stable_id = client
.get_connection(remote_endpoint_id)
.await
.expect("current transport")
.stable_id() as u64;
let local_node_id = client.current_node_id().await.expect("local node id");
let connection_id =
Client::deterministic_connection_id(&local_node_id, &remote_endpoint_id.to_string());
client
.connection_manager
.upsert_pending(
connection_id.clone(),
Some(remote_endpoint_id.to_string()),
Some("remote-device".to_string()),
Some(remote_endpoint_id.to_string()),
)
.await;
client
.connection_manager
.set_connected_with_transport(
&connection_id,
Some(remote_endpoint_id.to_string()),
Some(stable_id),
Some("incoming".to_string()),
)
.await
.expect("managed transport record");
let inbound_token = "inline-inbound-token";
client.register_session_token(inbound_token.to_string(), "user-device".to_string(), 0);
client
.validate_session_token_for_connection(inbound_token, &connection_id)
.await
.expect("accepted inbound epoch");
assert!(
client
.bind_session_admission_authoritative_device_id(&connection_id, "remote-device",)
.await
);
*client
.auto_connect_loop_key
.lock()
.expect("active session identity") =
Some(("user-1".to_string(), "local-device".to_string()));
let token = "inline-reciprocal-token";
let compound =
crate::session_token::build_compound_ticket("remote-ticket", token, "user-device", 0);
let (_, token_payload) = crate::session_token::split_compound_ticket(&compound);
client
.prepare_inline_reciprocal_session_admission(
remote_endpoint_id,
stable_id,
"stream-current",
"presentation-current",
token,
token_payload.expect("token payload"),
"local-device",
crate::native_protocol::SessionTokenStreamContract::OneShotAdmission,
)
.await
.expect("current transcript must prepare");
client
.confirm_inline_reciprocal_session_admission(
remote_endpoint_id,
stable_id,
"stream-current",
"presentation-current",
true,
Some("user-device"),
)
.await
.expect("current ACK must commit");
assert!(
client
.has_current_remote_session_admission_proof(
&connection_id,
remote_endpoint_id,
token,
)
.await
);
client
.prepare_inline_reciprocal_session_admission(
remote_endpoint_id,
stable_id,
"stream-stale",
"presentation-stale",
token,
token_payload.expect("token payload"),
"local-device",
crate::native_protocol::SessionTokenStreamContract::OneShotAdmission,
)
.await
.expect("second transcript must prepare");
let wrong_stream = client
.confirm_inline_reciprocal_session_admission(
remote_endpoint_id,
stable_id,
"different-stream",
"presentation-stale",
true,
Some("user-device"),
)
.await;
assert!(
wrong_stream.is_err(),
"same-generation ACK from another stream must fail closed"
);
let stale = client
.confirm_inline_reciprocal_session_admission(
remote_endpoint_id,
stable_id + 1,
"stream-stale",
"presentation-stale",
true,
Some("user-device"),
)
.await;
assert!(stale.is_err(), "retired generation must fail closed");
assert!(
!client
.has_current_remote_session_admission_proof(
&connection_id,
remote_endpoint_id,
"different-token",
)
.await
);
client
.prepare_inline_reciprocal_session_admission(
remote_endpoint_id,
stable_id,
"stream-scope",
"presentation-scope",
token,
token_payload.expect("token payload"),
"local-device",
crate::native_protocol::SessionTokenStreamContract::OneShotAdmission,
)
.await
.expect("scope transcript must prepare");
assert!(
client
.confirm_inline_reciprocal_session_admission(
remote_endpoint_id,
stable_id,
"stream-scope",
"presentation-scope",
true,
Some("drive"),
)
.await
.is_err(),
"ACK scope must match the prepared token payload"
);
client
.prepare_inline_reciprocal_session_admission(
remote_endpoint_id,
stable_id,
"stream-epoch",
"presentation-epoch",
token,
token_payload.expect("token payload"),
"local-device",
crate::native_protocol::SessionTokenStreamContract::OneShotAdmission,
)
.await
.expect("epoch transcript must prepare");
client.session_token_registry.mark_accepted(
&connection_id,
crate::session_token::SessionAdmissionMechanism::SessionToken,
Some(crate::session_token::GrantScope::from("user-device")),
Some("remote-device".to_string()),
);
assert!(
client
.confirm_inline_reciprocal_session_admission(
remote_endpoint_id,
stable_id,
"stream-epoch",
"presentation-epoch",
true,
Some("user-device"),
)
.await
.is_err(),
"an admission replacement must retire the prepared transcript even when its token fingerprint is unchanged"
);
client
.prepare_inline_reciprocal_session_admission(
remote_endpoint_id,
stable_id,
"stream-local-identity",
"presentation-local-identity",
token,
token_payload.expect("token payload"),
"local-device",
crate::native_protocol::SessionTokenStreamContract::OneShotAdmission,
)
.await
.expect("local identity transcript must prepare");
*client
.auto_connect_loop_key
.lock()
.expect("active session identity") =
Some(("user-1".to_string(), "replacement-local-device".to_string()));
assert!(
client
.confirm_inline_reciprocal_session_admission(
remote_endpoint_id,
stable_id,
"stream-local-identity",
"presentation-local-identity",
true,
Some("user-device"),
)
.await
.is_err(),
"a local identity replacement must retire the prepared transcript"
);
*client
.auto_connect_loop_key
.lock()
.expect("active session identity") =
Some(("user-1".to_string(), "local-device".to_string()));
client
.prepare_inline_reciprocal_session_admission(
remote_endpoint_id,
stable_id,
"stream-remote-identity",
"presentation-remote-identity",
token,
token_payload.expect("token payload"),
"local-device",
crate::native_protocol::SessionTokenStreamContract::OneShotAdmission,
)
.await
.expect("remote identity transcript must prepare");
client.observe_authoritative_device_node(
"replacement-remote-device",
&remote_endpoint_id.to_string(),
);
assert!(
client
.confirm_inline_reciprocal_session_admission(
remote_endpoint_id,
stable_id,
"stream-remote-identity",
"presentation-remote-identity",
true,
Some("user-device"),
)
.await
.is_err(),
"a remote identity replacement must retire the prepared transcript"
);
client.observe_authoritative_device_node("remote-device", &remote_endpoint_id.to_string());
client
.prepare_inline_reciprocal_session_admission(
remote_endpoint_id,
stable_id,
"stream-logout",
"presentation-logout",
token,
token_payload.expect("token payload"),
"local-device",
crate::native_protocol::SessionTokenStreamContract::OneShotAdmission,
)
.await
.expect("logout transcript must prepare");
client.clear_session_tokens();
assert!(
client
.confirm_inline_reciprocal_session_admission(
remote_endpoint_id,
stable_id,
"stream-logout",
"presentation-logout",
true,
Some("user-device"),
)
.await
.is_err(),
"logout must retire pending reciprocal transcripts"
);
client.register_session_token(inbound_token.to_string(), "user-device".to_string(), 0);
client
.validate_session_token_for_connection(inbound_token, &connection_id)
.await
.expect("re-accepted inbound epoch");
assert!(
client
.bind_session_admission_authoritative_device_id(&connection_id, "remote-device",)
.await
);
client
.prepare_inline_reciprocal_session_admission(
remote_endpoint_id,
stable_id,
"stream-revoke",
"presentation-revoke",
token,
token_payload.expect("token payload"),
"local-device",
crate::native_protocol::SessionTokenStreamContract::OneShotAdmission,
)
.await
.expect("revoke transcript must prepare");
client.revoke_session_token(inbound_token);
assert!(
client
.confirm_inline_reciprocal_session_admission(
remote_endpoint_id,
stable_id,
"stream-revoke",
"presentation-revoke",
true,
Some("user-device"),
)
.await
.is_err(),
"token revocation must retire pending reciprocal transcripts"
);
}
#[tokio::test]
async fn native_peers_complete_inline_reciprocal_admission_on_one_stream() {
let dialer = Client::new_with_app_tag(
crate::test_constants::TEST_PROJECT_ID.to_string(),
"inline-native-dialer".to_string(),
Box::new(|| None),
);
let host = Client::new_with_app_tag(
crate::test_constants::TEST_PROJECT_ID.to_string(),
"inline-native-host".to_string(),
Box::new(|| None),
);
let host_endpoint = Endpoint::builder(iroh::endpoint::presets::N0)
.alpns(vec![crate::native_node::PlutoniumProtocol::ALPN.to_vec()])
.relay_mode(RelayMode::Disabled)
.bind_addr(
"127.0.0.1:0"
.parse::<std::net::SocketAddr>()
.expect("host bind addr"),
)
.expect("host endpoint builder")
.bind()
.await
.expect("host endpoint");
let host_endpoint_id = host_endpoint.id();
let host_addr = wait_for_endpoint_addr(&host_endpoint, 5_000)
.await
.expect("host endpoint addr");
host.adopt_endpoint(host_endpoint).await;
let dialer_endpoint = Endpoint::builder(iroh::endpoint::presets::N0)
.alpns(vec![crate::native_node::PlutoniumProtocol::ALPN.to_vec()])
.relay_mode(RelayMode::Disabled)
.bind_addr(
"127.0.0.1:0"
.parse::<std::net::SocketAddr>()
.expect("dialer bind addr"),
)
.expect("dialer endpoint builder")
.bind()
.await
.expect("dialer endpoint");
let dialer_endpoint_id = dialer_endpoint.id();
dialer.adopt_endpoint(dialer_endpoint).await;
*dialer
.auto_connect_loop_key
.lock()
.expect("dialer identity") = Some(("user-1".to_string(), "dialer-device".to_string()));
*host.auto_connect_loop_key.lock().expect("host identity") =
Some(("user-1".to_string(), "host-device".to_string()));
dialer.observe_authoritative_device_node("host-device", &host_endpoint_id.to_string());
host.observe_authoritative_device_node("dialer-device", &dialer_endpoint_id.to_string());
let host_token = "inline-native-host-token";
let dialer_token = "inline-native-dialer-token";
host.register_session_token(host_token.to_string(), "user-device".to_string(), 0);
dialer.register_session_token(dialer_token.to_string(), "user-device".to_string(), 0);
let host_compound = crate::session_token::build_compound_ticket(
"host-ticket",
host_token,
"user-device",
0,
);
let dialer_compound = crate::session_token::build_compound_ticket(
"dialer-ticket",
dialer_token,
"user-device",
0,
);
let (_, host_payload) = crate::session_token::split_compound_ticket(&host_compound);
let (_, dialer_payload) = crate::session_token::split_compound_ticket(&dialer_compound);
host.native_route_repair_credentials
.write()
.expect("host reciprocal credential")
.insert(
dialer_endpoint_id.to_string(),
crate::client::NativeRouteRepairCredential {
token: dialer_token.to_string(),
token_payload: dialer_payload.expect("dialer payload").to_string(),
authoritative_device_id: "dialer-device".to_string(),
},
);
dialer
.ensure_connected_addr(host_endpoint_id, host_addr)
.await
.expect("connected transport");
let dialer_node_id = dialer.current_node_id().await.expect("dialer node id");
let connection_id =
Client::deterministic_connection_id(&dialer_node_id, &host_endpoint_id.to_string());
let dialer_stable_id = dialer
.get_connection(host_endpoint_id)
.await
.expect("dialer transport")
.stable_id() as u64;
dialer
.connection_manager
.upsert_pending(
connection_id.clone(),
Some(host_endpoint_id.to_string()),
Some("host-device".to_string()),
Some(host_endpoint_id.to_string()),
)
.await;
dialer
.connection_manager
.set_connected_with_transport(
&connection_id,
Some(host_endpoint_id.to_string()),
Some(dialer_stable_id),
Some(crate::transport_label::IROH_LAN.to_string()),
)
.await
.expect("managed dialer transport");
dialer
.connection_manager
.set_health(
&connection_id,
crate::connection_manager::ConnectionHealth::Stale,
)
.await;
let scope = tokio::time::timeout(
Duration::from_secs(10),
dialer.present_and_accept_session_token_for_route_repair(
host_endpoint_id,
&connection_id,
host_token,
host_payload,
Some("host-device".to_string()),
false,
true,
),
)
.await
.expect("inline reciprocal exchange timeout")
.expect("inline reciprocal exchange");
assert_eq!(scope, "user-device");
assert!(
dialer.remote_session_token_admitted_for_transport(&connection_id, dialer_stable_id,)
);
assert!(
dialer.inbound_session_token_admitted_for_transport(&connection_id, dialer_stable_id,)
);
assert_eq!(
dialer
.connection_manager
.peer_snapshot(&connection_id)
.await
.expect("dialer peer snapshot")
.health,
crate::connection_manager::ConnectionHealth::Healthy,
"the presenting side must settle the current replacement after bilateral admission",
);
let host_connection_id = Client::deterministic_connection_id(
&host.current_node_id().await.expect("host node id"),
&dialer_endpoint_id.to_string(),
);
let host_stable_id = host
.get_connection(dialer_endpoint_id)
.await
.expect("host transport")
.stable_id() as u64;
tokio::time::timeout(Duration::from_secs(2), async {
while !host
.remote_session_token_admitted_for_transport(&host_connection_id, host_stable_id)
{
tokio::time::sleep(Duration::from_millis(10)).await;
}
})
.await
.expect("host reciprocal proof must settle");
assert!(
host.remote_session_token_admitted_for_transport(&host_connection_id, host_stable_id,)
);
assert!(
host.inbound_session_token_admitted_for_transport(&host_connection_id, host_stable_id,)
);
}
#[tokio::test]
async fn auto_connect_represents_session_token_on_existing_live_transport() {
let dialer = std::sync::Arc::new(Client::new_with_app_tag(
crate::test_constants::TEST_PROJECT_ID.to_string(),
"existing-live-dialer".to_string(),
Box::new(|| None),
));
let host = std::sync::Arc::new(Client::new_with_app_tag(
crate::test_constants::TEST_PROJECT_ID.to_string(),
"existing-live-host".to_string(),
Box::new(|| None),
));
let dialer_endpoint = Endpoint::builder(iroh::endpoint::presets::N0)
.alpns(vec![crate::native_node::PlutoniumProtocol::ALPN.to_vec()])
.relay_mode(RelayMode::Disabled)
.bind_addr(
"127.0.0.1:0"
.parse::<std::net::SocketAddr>()
.expect("dialer bind addr"),
)
.expect("dialer endpoint builder")
.bind()
.await
.expect("dialer endpoint");
let host_endpoint = Endpoint::builder(iroh::endpoint::presets::N0)
.alpns(vec![crate::native_node::PlutoniumProtocol::ALPN.to_vec()])
.relay_mode(RelayMode::Disabled)
.bind_addr(
"127.0.0.1:0"
.parse::<std::net::SocketAddr>()
.expect("host bind addr"),
)
.expect("host endpoint builder")
.bind()
.await
.expect("host endpoint");
dialer.adopt_endpoint(dialer_endpoint).await;
host.adopt_endpoint(host_endpoint).await;
dialer.ensure_default_admission_gate("__startup");
let host_endpoint = host.get_endpoint().await.expect("host endpoint handle");
let host_endpoint_id = host_endpoint.id();
let host_addr = wait_for_endpoint_addr(&host_endpoint, 5_000)
.await
.expect("host endpoint addr");
let host_ticket = host.endpoint_ticket().await.expect("host ticket");
let token = "existing-live-user-device-token";
host.register_session_token(token.to_string(), "user-device".to_string(), 0);
let compound_ticket = crate::session_token::build_compound_ticket(
host_ticket.as_str(),
token,
"user-device",
0,
);
let dialer_endpoint = dialer.get_endpoint().await.expect("dialer endpoint handle");
let dialer_endpoint_id = dialer_endpoint.id();
let dialer_ticket = dialer.endpoint_ticket().await.expect("dialer ticket");
let reverse_token = "existing-live-reverse-user-device-token";
dialer.register_session_token(reverse_token.to_string(), "user-device".to_string(), 0);
let reverse_compound_ticket = crate::session_token::build_compound_ticket(
dialer_ticket.as_str(),
reverse_token,
"user-device",
0,
);
dialer
.ensure_connected_addr(host_endpoint_id, host_addr)
.await
.expect("establish live transport before auto-connect");
let dialer_node_id = dialer.current_node_id().await.expect("dialer node id");
let connection_id =
Client::deterministic_connection_id(&dialer_node_id, &host_endpoint_id.to_string());
dialer
.connection_manager
.set_device_id(&connection_id, "existing-live-host-device".to_string())
.await;
assert!(matches!(
dialer.session_admission(&connection_id),
crate::session_token::SessionAdmission::Pending
));
let remote_device_id = "existing-live-host-device";
let device = make_test_device(
remote_device_id,
&host_endpoint_id.to_string(),
&compound_ticket,
);
let reverse_device = make_test_device(
"existing-live-dialer-device",
&dialer_endpoint_id.to_string(),
&reverse_compound_ticket,
);
let (
mut last_attempt_at,
mut failure_count,
mut failure_backoff_until,
mut non_initiator_wait_started_at,
mut non_initiator_escalation_count,
mut skip_log_at,
mut connected_health_checked_at,
mut connected_health_state,
mut connected_health_failures,
mut last_presence_republish_at_ms,
mut last_network_change_at_ms,
mut last_known_node_id,
) = make_loop_state();
let (
mut host_last_attempt_at,
mut host_failure_count,
mut host_failure_backoff_until,
mut host_non_initiator_wait_started_at,
mut host_non_initiator_escalation_count,
mut host_skip_log_at,
mut host_connected_health_checked_at,
mut host_connected_health_state,
mut host_connected_health_failures,
mut host_last_presence_republish_at_ms,
mut host_last_network_change_at_ms,
mut host_last_known_node_id,
) = make_loop_state();
dialer
.try_auto_connect_device(
"user-1",
device.clone(),
"dialer-device",
&mut last_attempt_at,
&mut failure_count,
&mut failure_backoff_until,
500,
&mut non_initiator_wait_started_at,
&mut non_initiator_escalation_count,
3_000,
&mut skip_log_at,
&mut connected_health_checked_at,
&mut connected_health_state,
&mut connected_health_failures,
5_000,
3,
&mut last_presence_republish_at_ms,
60_000,
&mut last_network_change_at_ms,
30_000,
2,
&mut last_known_node_id,
)
.await;
host.try_auto_connect_device(
"user-1",
reverse_device.clone(),
"host-device",
&mut host_last_attempt_at,
&mut host_failure_count,
&mut host_failure_backoff_until,
500,
&mut host_non_initiator_wait_started_at,
&mut host_non_initiator_escalation_count,
3_000,
&mut host_skip_log_at,
&mut host_connected_health_checked_at,
&mut host_connected_health_state,
&mut host_connected_health_failures,
5_000,
3,
&mut host_last_presence_republish_at_ms,
60_000,
&mut host_last_network_change_at_ms,
30_000,
2,
&mut host_last_known_node_id,
)
.await;
assert!(host
.session_token_registry
.is_session_token_admitted_for_connection(&connection_id));
assert!(matches!(
dialer.session_admission(&connection_id),
crate::session_token::SessionAdmission::Accepted {
mechanism: crate::session_token::SessionAdmissionMechanism::SessionToken,
..
}
));
assert!(
dialer
.has_current_remote_session_admission_proof(
&connection_id,
host_endpoint_id,
token,
)
.await
);
assert!(failure_count.is_empty());
assert!(failure_backoff_until.is_empty());
assert_eq!(dialer.session_admission_block_reason(&connection_id), None);
assert_eq!(host.session_admission_block_reason(&connection_id), None);
let record = dialer
.connection_manager
.get_by_connection_id(&connection_id)
.await
.expect("managed connection record");
assert_eq!(record.device_id.as_deref(), Some(remote_device_id));
assert_eq!(record.state, ConnectionState::Connected);
let transport_stable_id = dialer
.get_connection(host_endpoint_id)
.await
.expect("live transport before proof invalidation")
.stable_id() as u64;
let host_transport_stable_id = host
.get_connection(dialer_endpoint_id)
.await
.expect("host live transport before proof invalidation")
.stable_id() as u64;
assert!(dialer.invalidate_native_main_route_proofs_for_transport(
&connection_id,
transport_stable_id,
));
assert!(host.invalidate_native_main_route_proofs_for_transport(
&connection_id,
host_transport_stable_id,
));
let application_key = [17u8; 32];
dialer.set_connection_application_crypto_key(&connection_id, application_key);
host.set_connection_application_crypto_key(&connection_id, application_key);
let connection = dialer
.get_connection(host_endpoint_id)
.await
.expect("live connection for unacknowledged verdict");
let (mut send, mut recv) = connection
.open_bi()
.await
.expect("open one-shot admission stream");
let presentation =
NativeMainMessage::session_token_presentation_with_payload_and_device_id(
token,
None,
Some("dialer-device"),
)
.with_session_token_stream_contract(
crate::native_protocol::SessionTokenStreamContract::OneShotAdmission,
);
let serialized = serde_json::to_vec(&presentation).expect("serialize presentation");
let mut frame = Vec::with_capacity(1 + 4 + 4 + 4 + serialized.len());
frame.push(0x00);
frame.extend_from_slice(&(4u32).to_be_bytes());
frame.extend_from_slice(b"main");
frame.extend_from_slice(&(serialized.len() as u32).to_be_bytes());
frame.extend_from_slice(&serialized);
send.write_all(&frame)
.await
.expect("write one-shot presentation");
send.flush().await.expect("flush one-shot presentation");
let mut response_prefix = [0u8; 1];
tokio::time::timeout(
Duration::from_secs(2),
recv.read_exact(&mut response_prefix),
)
.await
.expect("host response timeout")
.expect("host response prefix");
assert_eq!(response_prefix, [0x00]);
let _ = send.finish();
drop(recv);
sleep(Duration::from_millis(100)).await;
assert!(!host.inbound_session_token_admitted_for_transport(
&connection_id,
host_transport_stable_id,
));
assert_eq!(
dialer.session_admission_block_reason(&connection_id),
Some((false, "native-main-route-pending".to_string())),
);
dialer
.try_auto_connect_device(
"user-1",
device.clone(),
"dialer-device",
&mut last_attempt_at,
&mut failure_count,
&mut failure_backoff_until,
500,
&mut non_initiator_wait_started_at,
&mut non_initiator_escalation_count,
3_000,
&mut skip_log_at,
&mut connected_health_checked_at,
&mut connected_health_state,
&mut connected_health_failures,
5_000,
3,
&mut last_presence_republish_at_ms,
60_000,
&mut last_network_change_at_ms,
30_000,
2,
&mut last_known_node_id,
)
.await;
assert_eq!(
dialer.session_admission_block_reason(&connection_id),
Some((false, "native-main-route-pending".to_string())),
);
host.try_auto_connect_device(
"user-1",
reverse_device,
"host-device",
&mut host_last_attempt_at,
&mut host_failure_count,
&mut host_failure_backoff_until,
500,
&mut host_non_initiator_wait_started_at,
&mut host_non_initiator_escalation_count,
3_000,
&mut host_skip_log_at,
&mut host_connected_health_checked_at,
&mut host_connected_health_state,
&mut host_connected_health_failures,
5_000,
3,
&mut host_last_presence_republish_at_ms,
60_000,
&mut host_last_network_change_at_ms,
30_000,
2,
&mut host_last_known_node_id,
)
.await;
tokio::time::timeout(Duration::from_secs(2), async {
while dialer
.session_admission_block_reason(&connection_id)
.is_some()
|| host
.session_admission_block_reason(&connection_id)
.is_some()
{
sleep(Duration::from_millis(10)).await;
}
})
.await
.expect("bilateral one-shot admission should settle after the acknowledged retry");
assert_eq!(dialer.session_admission_block_reason(&connection_id), None);
assert_eq!(host.session_admission_block_reason(&connection_id), None);
assert!(dialer
.has_current_remote_session_admission_proof(
&connection_id,
host_endpoint_id,
token,
)
.await);
tokio::time::timeout(
Duration::from_secs(2),
dialer.try_auto_connect_device(
"user-1",
device.clone(),
"dialer-device",
&mut last_attempt_at,
&mut failure_count,
&mut failure_backoff_until,
500,
&mut non_initiator_wait_started_at,
&mut non_initiator_escalation_count,
3_000,
&mut skip_log_at,
&mut connected_health_checked_at,
&mut connected_health_state,
&mut connected_health_failures,
5_000,
3,
&mut last_presence_republish_at_ms,
60_000,
&mut last_network_change_at_ms,
30_000,
2,
&mut last_known_node_id,
),
)
.await
.expect("current remote admission proof must suppress duplicate presentation");
let rotated_token = "rotated-existing-live-user-device-token";
host.register_session_token(rotated_token.to_string(), "user-device".to_string(), 0);
let rotated_compound_ticket = crate::session_token::build_compound_ticket(
host_ticket.as_str(),
rotated_token,
"user-device",
0,
);
let rotated_device = make_test_device(
remote_device_id,
&host_endpoint_id.to_string(),
&rotated_compound_ticket,
);
tokio::time::timeout(
Duration::from_secs(2),
dialer.try_auto_connect_device(
"user-1",
rotated_device,
"dialer-device",
&mut last_attempt_at,
&mut failure_count,
&mut failure_backoff_until,
500,
&mut non_initiator_wait_started_at,
&mut non_initiator_escalation_count,
3_000,
&mut skip_log_at,
&mut connected_health_checked_at,
&mut connected_health_state,
&mut connected_health_failures,
5_000,
3,
&mut last_presence_republish_at_ms,
60_000,
&mut last_network_change_at_ms,
30_000,
2,
&mut last_known_node_id,
),
)
.await
.expect("rotated ticket must not reopen admission on the current route");
assert!(dialer
.has_current_remote_session_admission_proof(
&connection_id,
host_endpoint_id,
token,
)
.await);
assert!(
!dialer
.has_current_remote_session_admission_proof(
&connection_id,
host_endpoint_id,
rotated_token,
)
.await
);
}
#[tokio::test]
async fn auto_connect_loop_skips_excluded_device_as_initiator() {
let ep_local = Endpoint::builder(iroh::endpoint::presets::N0)
.relay_mode(RelayMode::Disabled)
.bind()
.await
.expect("local endpoint");
let ep_remote = Endpoint::builder(iroh::endpoint::presets::N0)
.relay_mode(RelayMode::Disabled)
.bind()
.await
.expect("remote endpoint");
let remote_addr = wait_for_endpoint_addr(&ep_remote, 3_000)
.await
.expect("remote addr");
let id_a = ep_local.id().to_string();
let id_b = ep_remote.id().to_string();
let (local_node_id_str, remote_node_id_str) = if id_a > id_b {
(id_a, id_b)
} else {
(id_b, id_a)
};
let ticket = iroh_tickets::endpoint::EndpointTicket::new(remote_addr);
let ticket_str = ticket.to_string();
let remote_device_id = "53874465-3225-407b-8012-21292c3835b8";
let client = std::sync::Arc::new(Client::new_with_app_tag(
crate::test_constants::TEST_PROJECT_ID.to_string(),
"test-app".to_string(),
Box::new(|| None),
));
*client.node_id.write().await = Some(local_node_id_str.clone());
let device = make_test_device(remote_device_id, &remote_node_id_str, &ticket_str);
client.set_auto_connect_excluded(remote_device_id, true);
let (
mut last_attempt_at,
mut failure_count,
mut failure_backoff_until,
mut non_initiator_wait_started_at,
mut non_initiator_escalation_count,
mut skip_log_at,
mut connected_health_checked_at,
mut connected_health_state,
mut connected_health_failures,
mut last_presence_republish_at_ms,
mut last_network_change_at_ms,
mut last_known_node_id,
) = make_loop_state();
client
.try_auto_connect_device(
"user-1",
device.clone(),
"local-device",
&mut last_attempt_at,
&mut failure_count,
&mut failure_backoff_until,
500,
&mut non_initiator_wait_started_at,
&mut non_initiator_escalation_count,
30_000,
&mut skip_log_at,
&mut connected_health_checked_at,
&mut connected_health_state,
&mut connected_health_failures,
5_000,
3,
&mut last_presence_republish_at_ms,
60_000,
&mut last_network_change_at_ms,
30_000,
2,
&mut last_known_node_id,
)
.await;
assert!(
!last_attempt_at.contains_key(remote_device_id),
"initiator-role excluded device must not appear in last_attempt_at"
);
client.set_auto_connect_excluded(remote_device_id, false);
assert!(!client.is_auto_connect_excluded(remote_device_id));
ep_local.close().await;
ep_remote.close().await;
}
#[tokio::test]
async fn auto_connect_loop_skips_node_alias_excluded_peer_with_different_device_id() {
let ep_local = Endpoint::builder(iroh::endpoint::presets::N0)
.relay_mode(RelayMode::Disabled)
.bind()
.await
.expect("local endpoint");
let ep_remote = Endpoint::builder(iroh::endpoint::presets::N0)
.relay_mode(RelayMode::Disabled)
.bind()
.await
.expect("remote endpoint");
let remote_addr = wait_for_endpoint_addr(&ep_remote, 3_000)
.await
.expect("remote addr");
let local_node_id_str = ep_local.id().to_string();
let remote_node_id_str = ep_remote.id().to_string();
let ticket = iroh_tickets::endpoint::EndpointTicket::new(remote_addr);
let ticket_str = ticket.to_string();
let old_remote_device_id = "manual-disconnected-device";
let stale_remote_device_id = "stale-presence-device";
let client = std::sync::Arc::new(Client::new_with_app_tag(
crate::test_constants::TEST_PROJECT_ID.to_string(),
"test-app".to_string(),
Box::new(|| None),
));
*client.node_id.write().await = Some(local_node_id_str.clone());
client.set_auto_connect_excluded_peer(
old_remote_device_id,
Some(&remote_node_id_str),
true,
);
let device = make_test_device(stale_remote_device_id, &remote_node_id_str, &ticket_str);
let (
mut last_attempt_at,
mut failure_count,
mut failure_backoff_until,
mut non_initiator_wait_started_at,
mut non_initiator_escalation_count,
mut skip_log_at,
mut connected_health_checked_at,
mut connected_health_state,
mut connected_health_failures,
mut last_presence_republish_at_ms,
mut last_network_change_at_ms,
mut last_known_node_id,
) = make_loop_state();
client
.try_auto_connect_device(
"user-1",
device,
"local-device",
&mut last_attempt_at,
&mut failure_count,
&mut failure_backoff_until,
500,
&mut non_initiator_wait_started_at,
&mut non_initiator_escalation_count,
0,
&mut skip_log_at,
&mut connected_health_checked_at,
&mut connected_health_state,
&mut connected_health_failures,
5_000,
3,
&mut last_presence_republish_at_ms,
60_000,
&mut last_network_change_at_ms,
30_000,
2,
&mut last_known_node_id,
)
.await;
assert!(
!last_attempt_at.contains_key(stale_remote_device_id),
"node-id alias should suppress stale/different device ids for the same peer"
);
assert_eq!(
client.current_excluded_peers_snapshot(),
vec![old_remote_device_id.to_string()],
"published excludedPeers must remain device-id scoped and not include node aliases"
);
ep_local.close().await;
ep_remote.close().await;
}
#[tokio::test]
async fn auto_connect_loop_skips_excluded_device_as_non_initiator() {
let ep_local = Endpoint::builder(iroh::endpoint::presets::N0)
.relay_mode(RelayMode::Disabled)
.bind()
.await
.expect("local endpoint");
let ep_remote = Endpoint::builder(iroh::endpoint::presets::N0)
.relay_mode(RelayMode::Disabled)
.bind()
.await
.expect("remote endpoint");
let remote_addr = wait_for_endpoint_addr(&ep_remote, 3_000)
.await
.expect("remote addr");
let id_a = ep_local.id().to_string();
let id_b = ep_remote.id().to_string();
let (local_node_id_str, remote_node_id_str) = if id_a < id_b {
(id_a, id_b)
} else {
(id_b, id_a)
};
let ticket = iroh_tickets::endpoint::EndpointTicket::new(remote_addr);
let ticket_str = ticket.to_string();
let remote_device_id = "non-initiator-device-test";
let client = std::sync::Arc::new(Client::new_with_app_tag(
crate::test_constants::TEST_PROJECT_ID.to_string(),
"test-app".to_string(),
Box::new(|| None),
));
*client.node_id.write().await = Some(local_node_id_str.clone());
let device = make_test_device(remote_device_id, &remote_node_id_str, &ticket_str);
client.set_auto_connect_excluded(remote_device_id, true);
let (
mut last_attempt_at,
mut failure_count,
mut failure_backoff_until,
mut non_initiator_wait_started_at,
mut non_initiator_escalation_count,
mut skip_log_at,
mut connected_health_checked_at,
mut connected_health_state,
mut connected_health_failures,
mut last_presence_republish_at_ms,
mut last_network_change_at_ms,
mut last_known_node_id,
) = make_loop_state();
for _ in 0..2 {
client
.try_auto_connect_device(
"user-1",
device.clone(),
"local-device",
&mut last_attempt_at,
&mut failure_count,
&mut failure_backoff_until,
500,
&mut non_initiator_wait_started_at,
&mut non_initiator_escalation_count,
0, &mut skip_log_at,
&mut connected_health_checked_at,
&mut connected_health_state,
&mut connected_health_failures,
5_000,
3,
&mut last_presence_republish_at_ms,
60_000,
&mut last_network_change_at_ms,
30_000,
2,
&mut last_known_node_id,
)
.await;
}
assert!(
!last_attempt_at.contains_key(remote_device_id),
"non-initiator excluded device must not appear in last_attempt_at"
);
client.set_auto_connect_excluded(remote_device_id, false);
assert!(!client.is_auto_connect_excluded(remote_device_id));
ep_local.close().await;
ep_remote.close().await;
}
#[tokio::test]
async fn auto_connect_skips_remote_excluded_local_device() {
let ep_local = Endpoint::builder(iroh::endpoint::presets::N0)
.relay_mode(RelayMode::Disabled)
.bind()
.await
.expect("local endpoint");
let ep_remote = Endpoint::builder(iroh::endpoint::presets::N0)
.relay_mode(RelayMode::Disabled)
.bind()
.await
.expect("remote endpoint");
let addr_a = wait_for_endpoint_addr(&ep_local, 3_000)
.await
.expect("local addr");
let addr_b = wait_for_endpoint_addr(&ep_remote, 3_000)
.await
.expect("remote addr");
let id_a = ep_local.id().to_string();
let id_b = ep_remote.id().to_string();
let (local_node_id_str, remote_node_id_str, remote_addr) = if id_a > id_b {
(id_a, id_b, addr_b)
} else {
(id_b, id_a, addr_a)
};
let ticket = iroh_tickets::endpoint::EndpointTicket::new(remote_addr);
let ticket_str = ticket.to_string();
let remote_device_id = "remote-excluder-device";
let local_device_id = "my-local-device";
let client = std::sync::Arc::new(Client::new_with_app_tag(
crate::test_constants::TEST_PROJECT_ID.to_string(),
"test-app".to_string(),
Box::new(|| None),
));
*client.node_id.write().await = Some(local_node_id_str.clone());
let device = make_test_device_with_excluded(
remote_device_id,
&remote_node_id_str,
&ticket_str,
vec![local_device_id.to_string()],
);
assert!(!client.is_auto_connect_excluded(remote_device_id));
let (
mut last_attempt_at,
mut failure_count,
mut failure_backoff_until,
mut non_initiator_wait_started_at,
mut non_initiator_escalation_count,
mut skip_log_at,
mut connected_health_checked_at,
mut connected_health_state,
mut connected_health_failures,
mut last_presence_republish_at_ms,
mut last_network_change_at_ms,
mut last_known_node_id,
) = make_loop_state();
client
.try_auto_connect_device(
"user-1",
device.clone(),
local_device_id,
&mut last_attempt_at,
&mut failure_count,
&mut failure_backoff_until,
500,
&mut non_initiator_wait_started_at,
&mut non_initiator_escalation_count,
0,
&mut skip_log_at,
&mut connected_health_checked_at,
&mut connected_health_state,
&mut connected_health_failures,
5_000,
3,
&mut last_presence_republish_at_ms,
60_000,
&mut last_network_change_at_ms,
30_000,
2,
&mut last_known_node_id,
)
.await;
assert!(
!last_attempt_at.contains_key(remote_device_id),
"remote excluded_peers containing local device_id must suppress auto-connect"
);
ep_local.close().await;
ep_remote.close().await;
}
#[tokio::test]
async fn auto_connect_not_skipped_when_remote_excludes_different_device() {
let ep_local = Endpoint::builder(iroh::endpoint::presets::N0)
.relay_mode(RelayMode::Disabled)
.bind()
.await
.expect("local endpoint");
let ep_remote = Endpoint::builder(iroh::endpoint::presets::N0)
.relay_mode(RelayMode::Disabled)
.bind()
.await
.expect("remote endpoint");
let addr_a = wait_for_endpoint_addr(&ep_local, 3_000)
.await
.expect("local addr");
let addr_b = wait_for_endpoint_addr(&ep_remote, 3_000)
.await
.expect("remote addr");
let id_a = ep_local.id().to_string();
let id_b = ep_remote.id().to_string();
let (local_node_id_str, remote_node_id_str, remote_addr) = if id_a > id_b {
(id_a, id_b, addr_b)
} else {
(id_b, id_a, addr_a)
};
let ticket = iroh_tickets::endpoint::EndpointTicket::new(remote_addr);
let ticket_str = ticket.to_string();
let remote_device_id = "not-excluding-device";
let local_device_id = "my-local-device";
let client = std::sync::Arc::new(Client::new_with_app_tag(
crate::test_constants::TEST_PROJECT_ID.to_string(),
"test-app".to_string(),
Box::new(|| None),
));
*client.node_id.write().await = Some(local_node_id_str.clone());
let device = make_test_device_with_excluded(
remote_device_id,
&remote_node_id_str,
&ticket_str,
vec!["some-other-device".to_string()],
);
let (
mut last_attempt_at,
mut failure_count,
mut failure_backoff_until,
mut non_initiator_wait_started_at,
mut non_initiator_escalation_count,
mut skip_log_at,
mut connected_health_checked_at,
mut connected_health_state,
mut connected_health_failures,
mut last_presence_republish_at_ms,
mut last_network_change_at_ms,
mut last_known_node_id,
) = make_loop_state();
client
.try_auto_connect_device(
"user-1",
device.clone(),
local_device_id,
&mut last_attempt_at,
&mut failure_count,
&mut failure_backoff_until,
500,
&mut non_initiator_wait_started_at,
&mut non_initiator_escalation_count,
0,
&mut skip_log_at,
&mut connected_health_checked_at,
&mut connected_health_state,
&mut connected_health_failures,
5_000,
3,
&mut last_presence_republish_at_ms,
60_000,
&mut last_network_change_at_ms,
30_000,
2,
&mut last_known_node_id,
)
.await;
assert!(
last_attempt_at.contains_key(remote_device_id),
"dial should proceed when remote excluded_peers does not contain local device_id"
);
ep_local.close().await;
ep_remote.close().await;
}
#[tokio::test]
async fn auto_connect_skips_remote_excluded_local_device_case_insensitive_match() {
let ep_local = Endpoint::builder(iroh::endpoint::presets::N0)
.relay_mode(RelayMode::Disabled)
.bind()
.await
.expect("local endpoint");
let ep_remote = Endpoint::builder(iroh::endpoint::presets::N0)
.relay_mode(RelayMode::Disabled)
.bind()
.await
.expect("remote endpoint");
let addr_a = wait_for_endpoint_addr(&ep_local, 3_000)
.await
.expect("local addr");
let addr_b = wait_for_endpoint_addr(&ep_remote, 3_000)
.await
.expect("remote addr");
let id_a = ep_local.id().to_string();
let id_b = ep_remote.id().to_string();
let (local_node_id_str, remote_node_id_str, remote_addr) = if id_a > id_b {
(id_a, id_b, addr_b)
} else {
(id_b, id_a, addr_a)
};
let ticket = iroh_tickets::endpoint::EndpointTicket::new(remote_addr);
let ticket_str = ticket.to_string();
let remote_device_id = "case-test-remote";
let local_device_id = "MY-LOCAL-DEVICE";
let client = std::sync::Arc::new(Client::new_with_app_tag(
crate::test_constants::TEST_PROJECT_ID.to_string(),
"test-app".to_string(),
Box::new(|| None),
));
*client.node_id.write().await = Some(local_node_id_str.clone());
let device = make_test_device_with_excluded(
remote_device_id,
&remote_node_id_str,
&ticket_str,
vec!["my-local-device".to_string()],
);
let (
mut last_attempt_at,
mut failure_count,
mut failure_backoff_until,
mut non_initiator_wait_started_at,
mut non_initiator_escalation_count,
mut skip_log_at,
mut connected_health_checked_at,
mut connected_health_state,
mut connected_health_failures,
mut last_presence_republish_at_ms,
mut last_network_change_at_ms,
mut last_known_node_id,
) = make_loop_state();
client
.try_auto_connect_device(
"user-1",
device.clone(),
local_device_id,
&mut last_attempt_at,
&mut failure_count,
&mut failure_backoff_until,
500,
&mut non_initiator_wait_started_at,
&mut non_initiator_escalation_count,
0,
&mut skip_log_at,
&mut connected_health_checked_at,
&mut connected_health_state,
&mut connected_health_failures,
5_000,
3,
&mut last_presence_republish_at_ms,
60_000,
&mut last_network_change_at_ms,
30_000,
2,
&mut last_known_node_id,
)
.await;
assert!(
!last_attempt_at.contains_key(remote_device_id),
"remote excluded_peers must block by case-insensitive match"
);
ep_local.close().await;
ep_remote.close().await;
}
#[test]
fn current_excluded_peers_snapshot_reflects_set() {
let client = std::sync::Arc::new(Client::new_with_app_tag(
crate::test_constants::TEST_PROJECT_ID.to_string(),
"test-app".to_string(),
Box::new(|| None),
));
assert!(client.current_excluded_peers_snapshot().is_empty());
client.set_auto_connect_excluded("device-b", true);
client.set_auto_connect_excluded("device-a", true);
let snapshot = client.current_excluded_peers_snapshot();
assert_eq!(snapshot, vec!["device-a", "device-b"]);
client.set_auto_connect_excluded("device-a", false);
let snapshot2 = client.current_excluded_peers_snapshot();
assert_eq!(snapshot2, vec!["device-b"]);
}
#[tokio::test]
async fn connect_device_rejects_invalid_ticket_before_runtime_lookup() {
let client = Client::new_with_app_tag(
crate::test_constants::TEST_PROJECT_ID.to_string(),
"test-app".to_string(),
Box::new(|| None),
);
let error = client
.connect_device(Some("device-1"), "not-a-valid-ticket")
.await
.expect_err("invalid endpoint ticket should fail");
let message = error.to_string().to_ascii_lowercase();
assert!(
message.contains("endpoint") || message.contains("ticket"),
"unexpected connect_device error: {}",
error
);
}
#[tokio::test]
async fn connect_device_coalesces_concurrent_dials_for_same_endpoint() {
let client_a = Client::new_with_app_tag(
crate::test_constants::TEST_PROJECT_ID.to_string(),
"connect-coalesce-a".to_string(),
Box::new(|| None),
);
let client_b = Client::new_with_app_tag(
crate::test_constants::TEST_PROJECT_ID.to_string(),
"connect-coalesce-b".to_string(),
Box::new(|| None),
);
let endpoint_a = Endpoint::builder(iroh::endpoint::presets::N0)
.alpns(vec![crate::native_node::PlutoniumProtocol::ALPN.to_vec()])
.relay_mode(RelayMode::Disabled)
.bind_addr(
"127.0.0.1:0"
.parse::<std::net::SocketAddr>()
.expect("socket addr"),
)
.expect("bind addr a")
.bind()
.await
.expect("bind endpoint a");
let endpoint_b = Endpoint::builder(iroh::endpoint::presets::N0)
.alpns(vec![crate::native_node::PlutoniumProtocol::ALPN.to_vec()])
.relay_mode(RelayMode::Disabled)
.bind_addr(
"127.0.0.1:0"
.parse::<std::net::SocketAddr>()
.expect("socket addr"),
)
.expect("bind addr b")
.bind()
.await
.expect("bind endpoint b");
client_a.adopt_endpoint(endpoint_a).await;
client_b.adopt_endpoint(endpoint_b).await;
let endpoint_b = client_b.get_endpoint().await.expect("endpoint b");
let _ = wait_for_endpoint_addr(&endpoint_b, 5_000)
.await
.expect("endpoint b addr");
let ticket_b = client_b.endpoint_ticket().await.expect("endpoint ticket");
client_a.set_auto_connect_excluded_peer("device-b", None, true);
assert!(client_a.is_auto_connect_excluded("device-b"));
let (first, second) = tokio::join!(
client_a.connect_device(Some("device-b"), ticket_b.as_str()),
client_a.connect_device(Some("device-b"), ticket_b.as_str()),
);
let first = first.expect("first connect succeeds");
let second = second.expect("second connect coalesces");
assert!(
!client_a.is_auto_connect_excluded("device-b"),
"explicit connect_device must clear manual-disconnect suppression"
);
assert_eq!(first.connection_id, second.connection_id);
let record = client_a
.connection_manager
.get_by_connection_id(&first.connection_id)
.await
.expect("managed record");
assert_eq!(record.state, ConnectionState::Connected);
assert_eq!(
record.transport_generation, 1,
"coalesced connect and in-place direct-path selection must keep one physical transport generation"
);
assert_eq!(
record.replacement_count, 1,
"coalesced connect should not create a replacement transport"
);
assert_eq!(
client_a.connection_manager.list_all().await.len(),
1,
"duplicate connect callers should share the same managed record"
);
}
#[tokio::test]
async fn native_stream_open_redials_with_cached_ticket_endpoint_addr_after_cached_connection_drop(
) {
use crate::native_node::IncomingStreamType;
let client_a = Client::new_with_app_tag(
crate::test_constants::TEST_PROJECT_ID.to_string(),
"cached-redial-a".to_string(),
Box::new(|| None),
);
let client_b = Client::new_with_app_tag(
crate::test_constants::TEST_PROJECT_ID.to_string(),
"cached-redial-b".to_string(),
Box::new(|| None),
);
let endpoint_a = Endpoint::builder(iroh::endpoint::presets::N0)
.alpns(vec![crate::native_node::PlutoniumProtocol::ALPN.to_vec()])
.relay_mode(RelayMode::Disabled)
.bind_addr(
"127.0.0.1:0"
.parse::<std::net::SocketAddr>()
.expect("socket addr"),
)
.expect("bind addr a")
.bind()
.await
.expect("bind endpoint a");
let endpoint_b = Endpoint::builder(iroh::endpoint::presets::N0)
.alpns(vec![crate::native_node::PlutoniumProtocol::ALPN.to_vec()])
.relay_mode(RelayMode::Disabled)
.bind_addr(
"127.0.0.1:0"
.parse::<std::net::SocketAddr>()
.expect("socket addr"),
)
.expect("bind addr b")
.bind()
.await
.expect("bind endpoint b");
client_a.adopt_endpoint(endpoint_a).await;
client_b.adopt_endpoint(endpoint_b).await;
let endpoint_b = client_b.get_endpoint().await.expect("endpoint b");
let endpoint_b_id = endpoint_b.id();
let _ = wait_for_endpoint_addr(&endpoint_b, 5_000)
.await
.expect("endpoint b addr");
let incoming = client_b.incoming_streams().await.expect("incoming streams");
let ticket_b = client_b.endpoint_ticket().await.expect("endpoint ticket");
client_a
.connect_device(Some("device-b"), ticket_b.as_str())
.await
.expect("managed connect succeeds and caches endpoint addr");
client_a
.disconnect_with_reason(
endpoint_b_id,
crate::lifecycle_reason::REASON_NETWORK_CHANGE_RECONNECT,
)
.await
.expect("drop cached connection");
let (mut send, _recv) = client_a
.open_bi_internal(endpoint_b_id)
.await
.expect("open_bi should redial through cached endpoint addr");
send.write_all(b"cached-redial")
.await
.expect("write redial payload");
send.finish().expect("finish redial stream");
let incoming_stream = tokio::time::timeout(Duration::from_secs(5), incoming.recv())
.await
.expect("incoming stream timeout")
.expect("incoming stream");
assert!(
matches!(incoming_stream.stream, IncomingStreamType::Bi(_, _)),
"redialed stream should arrive over Iroh"
);
}
#[tokio::test]
async fn native_peer_bi_open_rejects_cached_route_without_live_application_transport() {
let client_a = Client::new_with_app_tag(
crate::test_constants::TEST_PROJECT_ID.to_string(),
"peer-redial-a".to_string(),
Box::new(|| None),
);
let client_b = Client::new_with_app_tag(
crate::test_constants::TEST_PROJECT_ID.to_string(),
"peer-redial-b".to_string(),
Box::new(|| None),
);
let endpoint_a = Endpoint::builder(iroh::endpoint::presets::N0)
.alpns(vec![crate::native_node::PlutoniumProtocol::ALPN.to_vec()])
.relay_mode(RelayMode::Disabled)
.bind_addr(
"127.0.0.1:0"
.parse::<std::net::SocketAddr>()
.expect("socket addr"),
)
.expect("bind addr a")
.bind()
.await
.expect("bind endpoint a");
let endpoint_b = Endpoint::builder(iroh::endpoint::presets::N0)
.alpns(vec![crate::native_node::PlutoniumProtocol::ALPN.to_vec()])
.relay_mode(RelayMode::Disabled)
.bind_addr(
"127.0.0.1:0"
.parse::<std::net::SocketAddr>()
.expect("socket addr"),
)
.expect("bind addr b")
.bind()
.await
.expect("bind endpoint b");
client_a.adopt_endpoint(endpoint_a).await;
client_b.adopt_endpoint(endpoint_b).await;
let endpoint_b = client_b.get_endpoint().await.expect("endpoint b");
let endpoint_b_id = endpoint_b.id();
let endpoint_b_addr = wait_for_endpoint_addr(&endpoint_b, 5_000)
.await
.expect("endpoint b addr");
client_a.remember_endpoint_addr(&endpoint_b_addr).await;
let connection_id = "conn-peer-bi-redial";
client_a
.connection_manager
.upsert_pending(
connection_id.to_string(),
Some(endpoint_b_id.to_string()),
Some("device-b".to_string()),
Some(endpoint_b_id.to_string()),
)
.await;
client_a
.connection_manager
.set_connected_with_transport(
connection_id,
Some(endpoint_b_id.to_string()),
None,
Some("test-settled-upgraded-route".to_string()),
)
.await
.expect("connected projection");
client_a
.connection_manager
.report_transport_status(
connection_id,
"webrtc".to_string(),
Some("iroh-relay".to_string()),
)
.await
.expect("settled upgraded route projection");
client_a
.connection_manager
.set_health(connection_id, ConnectionHealth::Healthy)
.await;
let error = client_a
.open_peer_bi("device-b", Some(5_000))
.await
.expect_err("cached endpoint address must not substitute for a live application route");
assert!(
error.to_string().contains("no active transport"),
"unexpected error: {}",
error
);
}
#[tokio::test]
async fn native_transport_only_stream_open_redials_with_cached_ticket_endpoint_addr_after_cached_connection_drop(
) {
use crate::native_node::IncomingStreamType;
let client_a = Client::new_with_app_tag(
crate::test_constants::TEST_PROJECT_ID.to_string(),
"transport-only-redial-a".to_string(),
Box::new(|| None),
);
let client_b = Client::new_with_app_tag(
crate::test_constants::TEST_PROJECT_ID.to_string(),
"transport-only-redial-b".to_string(),
Box::new(|| None),
);
let endpoint_a = Endpoint::builder(iroh::endpoint::presets::N0)
.alpns(vec![crate::native_node::PlutoniumProtocol::ALPN.to_vec()])
.relay_mode(RelayMode::Disabled)
.bind_addr(
"127.0.0.1:0"
.parse::<std::net::SocketAddr>()
.expect("socket addr"),
)
.expect("bind addr a")
.bind()
.await
.expect("bind endpoint a");
let endpoint_b = Endpoint::builder(iroh::endpoint::presets::N0)
.alpns(vec![crate::native_node::PlutoniumProtocol::ALPN.to_vec()])
.relay_mode(RelayMode::Disabled)
.bind_addr(
"127.0.0.1:0"
.parse::<std::net::SocketAddr>()
.expect("socket addr"),
)
.expect("bind addr b")
.bind()
.await
.expect("bind endpoint b");
client_a.adopt_endpoint(endpoint_a).await;
client_b.adopt_endpoint(endpoint_b).await;
let endpoint_b = client_b.get_endpoint().await.expect("endpoint b");
let endpoint_b_id = endpoint_b.id();
let _ = wait_for_endpoint_addr(&endpoint_b, 5_000)
.await
.expect("endpoint b addr");
let incoming = client_b.incoming_streams().await.expect("incoming streams");
let ticket_b = client_b.endpoint_ticket().await.expect("endpoint ticket");
client_a
.connect_device(Some("device-b"), ticket_b.as_str())
.await
.expect("managed connect succeeds and caches endpoint addr");
client_a
.disconnect_with_reason(
endpoint_b_id,
crate::lifecycle_reason::REASON_NETWORK_CHANGE_RECONNECT,
)
.await
.expect("drop cached connection");
let (_connection_id, _remote_node_id, mut send, _recv) = client_a
.open_peer_bi_transport_only("device-b", Some(5_000))
.await
.expect("transport-only open should redial through cached endpoint addr");
send.write_all(b"transport-only-redial")
.await
.expect("write redial payload");
send.finish().expect("finish redial stream");
let incoming_stream = tokio::time::timeout(Duration::from_secs(5), incoming.recv())
.await
.expect("incoming stream timeout")
.expect("incoming stream");
assert!(
matches!(incoming_stream.stream, IncomingStreamType::Bi(_, _)),
"transport-only redialed stream should arrive over Iroh"
);
}
#[tokio::test]
async fn connect_device_reports_connected_connection_state_on_return() {
let client_a = Client::new_with_app_tag(
crate::test_constants::TEST_PROJECT_ID.to_string(),
"connect-state-a".to_string(),
Box::new(|| None),
);
let client_b = Client::new_with_app_tag(
crate::test_constants::TEST_PROJECT_ID.to_string(),
"connect-state-b".to_string(),
Box::new(|| None),
);
let endpoint_a = Endpoint::builder(iroh::endpoint::presets::N0)
.alpns(vec![crate::native_node::PlutoniumProtocol::ALPN.to_vec()])
.relay_mode(RelayMode::Disabled)
.bind_addr(
"127.0.0.1:0"
.parse::<std::net::SocketAddr>()
.expect("socket addr"),
)
.expect("bind addr a")
.bind()
.await
.expect("bind endpoint a");
let endpoint_b = Endpoint::builder(iroh::endpoint::presets::N0)
.alpns(vec![crate::native_node::PlutoniumProtocol::ALPN.to_vec()])
.relay_mode(RelayMode::Disabled)
.bind_addr(
"127.0.0.1:0"
.parse::<std::net::SocketAddr>()
.expect("socket addr"),
)
.expect("bind addr b")
.bind()
.await
.expect("bind endpoint b");
client_a.adopt_endpoint(endpoint_a).await;
client_b.adopt_endpoint(endpoint_b).await;
let ticket_b = client_b.endpoint_ticket().await.expect("endpoint ticket");
let result = client_a
.connect_device(Some("device-b"), ticket_b.as_str())
.await
.expect("managed connect succeeds");
let record = client_a
.connection_manager
.get_by_connection_id(&result.connection_id)
.await
.expect("managed record");
assert_eq!(record.state, ConnectionState::Connected);
let snapshot = client_a
.connection_state(&result.connection_id)
.await
.expect("connection state snapshot");
assert_eq!(snapshot.state, "connected");
assert_eq!(snapshot.transport_state, "connected");
}
#[tokio::test]
async fn connect_device_rebinds_live_transport_after_stale_replacement_projection() {
let client_a = Client::new_with_app_tag(
crate::test_constants::TEST_PROJECT_ID.to_string(),
"connect-rebind-a".to_string(),
Box::new(|| None),
);
let client_b = Client::new_with_app_tag(
crate::test_constants::TEST_PROJECT_ID.to_string(),
"connect-rebind-b".to_string(),
Box::new(|| None),
);
let endpoint_a = Endpoint::builder(iroh::endpoint::presets::N0)
.alpns(vec![crate::native_node::PlutoniumProtocol::ALPN.to_vec()])
.relay_mode(RelayMode::Disabled)
.bind_addr(
"127.0.0.1:0"
.parse::<std::net::SocketAddr>()
.expect("socket addr"),
)
.expect("bind addr a")
.bind()
.await
.expect("bind endpoint a");
let endpoint_b = Endpoint::builder(iroh::endpoint::presets::N0)
.alpns(vec![crate::native_node::PlutoniumProtocol::ALPN.to_vec()])
.relay_mode(RelayMode::Disabled)
.bind_addr(
"127.0.0.1:0"
.parse::<std::net::SocketAddr>()
.expect("socket addr"),
)
.expect("bind addr b")
.bind()
.await
.expect("bind endpoint b");
client_a.adopt_endpoint(endpoint_a).await;
client_b.adopt_endpoint(endpoint_b).await;
let ticket_b = client_b.endpoint_ticket().await.expect("endpoint ticket");
let first = client_a
.connect_device(Some("device-b"), ticket_b.as_str())
.await
.expect("initial managed connect succeeds");
let live_stable_id = client_a
.get_connection(first.remote_node_id.parse().expect("remote endpoint id"))
.await
.map(|connection| connection.stable_id() as u64)
.expect("live transport stable id");
client_a
.connection_manager
.mark_transport_replaced(
&first.connection_id,
None,
Some("transient-disconnect".to_string()),
Some(crate::lifecycle_reason::REASON_REPLACEMENT_IN_PROGRESS.to_string()),
)
.await
.expect("stale replacement projection");
let stale = client_a
.connection_manager
.get_by_connection_id(&first.connection_id)
.await
.expect("stale managed record");
assert_eq!(stale.transport_stable_id, None);
assert_eq!(
stale.status_reason.as_deref(),
Some(crate::lifecycle_reason::REASON_REPLACEMENT_IN_PROGRESS)
);
let rebound = client_a
.connect_device(Some("device-b"), ticket_b.as_str())
.await
.expect("idempotent connect reconciles live transport");
assert_eq!(rebound.connection_id, first.connection_id);
let record = client_a
.connection_manager
.get_by_connection_id(&first.connection_id)
.await
.expect("reconciled managed record");
assert_eq!(record.state, ConnectionState::Connected);
assert_eq!(record.transport_stable_id, Some(live_stable_id));
assert_eq!(record.status_reason, None);
let session = client_a
.peer_session("device-b")
.await
.expect("reconciled peer session");
assert!(session.settled_ready, "live rebound transport must settle");
}
#[tokio::test]
async fn ensure_connected_addr_commits_connection_manager_before_return() {
let client_a = Client::new_with_app_tag(
crate::test_constants::TEST_PROJECT_ID.to_string(),
"ensure-connected-record-a".to_string(),
Box::new(|| None),
);
let client_b = Client::new_with_app_tag(
crate::test_constants::TEST_PROJECT_ID.to_string(),
"ensure-connected-record-b".to_string(),
Box::new(|| None),
);
let endpoint_a = Endpoint::builder(iroh::endpoint::presets::N0)
.alpns(vec![crate::native_node::PlutoniumProtocol::ALPN.to_vec()])
.relay_mode(RelayMode::Disabled)
.bind_addr(
"127.0.0.1:0"
.parse::<std::net::SocketAddr>()
.expect("socket addr"),
)
.expect("bind addr a")
.bind()
.await
.expect("bind endpoint a");
let endpoint_b = Endpoint::builder(iroh::endpoint::presets::N0)
.alpns(vec![crate::native_node::PlutoniumProtocol::ALPN.to_vec()])
.relay_mode(RelayMode::Disabled)
.bind_addr(
"127.0.0.1:0"
.parse::<std::net::SocketAddr>()
.expect("socket addr"),
)
.expect("bind addr b")
.bind()
.await
.expect("bind endpoint b");
client_a.adopt_endpoint(endpoint_a).await;
client_b.adopt_endpoint(endpoint_b).await;
let endpoint_b = client_b.get_endpoint().await.expect("endpoint b");
let endpoint_b_id = endpoint_b.id();
let endpoint_b_addr = wait_for_endpoint_addr(&endpoint_b, 5_000)
.await
.expect("endpoint b addr");
let remote_node_id = endpoint_b_id.to_string();
let local_node_id = client_a.current_node_id().await.expect("local node id");
let connection_id = Client::deterministic_connection_id(&local_node_id, &remote_node_id);
client_a
.connection_manager
.upsert_pending(
connection_id.clone(),
Some(remote_node_id.clone()),
Some("device-b".to_string()),
Some(remote_node_id.clone()),
)
.await;
client_a
.connection_manager
.set_connecting(&connection_id)
.await;
client_a
.ensure_connected_addr(endpoint_b_id, endpoint_b_addr)
.await
.expect("transport dial succeeds");
let record = client_a
.connection_manager
.get_by_connection_id(&connection_id)
.await
.expect("managed record exists before bi-stream open");
assert_eq!(record.state, ConnectionState::Connected);
let snapshot = client_a
.connection_state(&connection_id)
.await
.expect("connection state snapshot");
assert_eq!(snapshot.state, "connecting");
assert_eq!(snapshot.transport_state, "connected");
let (_send, _recv) = client_a
.open_bi(endpoint_b_id)
.await
.expect("bi-stream open succeeds after managed record commit");
}
#[tokio::test]
async fn resolve_peer_connection_ids_uses_core_peer_identity_aliases() {
let client = Client::new_with_app_tag(
crate::test_constants::TEST_PROJECT_ID.to_string(),
"test-app".to_string(),
Box::new(|| None),
);
client
.connection_manager
.upsert_pending(
"conn-1".to_string(),
Some("node1".to_string()),
Some("device-hint-1".to_string()),
Some("node1".to_string()),
)
.await;
client.connection_manager.set_connecting("conn-1").await;
client
.connection_manager
.set_device_id("conn-1", "device-1".to_string())
.await;
assert_eq!(
client.resolve_peer_connection_ids("device-1").await,
vec!["conn-1".to_string()]
);
assert_eq!(
client.resolve_peer_connection_ids("device-hint-1").await,
vec!["conn-1".to_string()]
);
assert_eq!(
client.resolve_peer_connection_ids("node1").await,
vec!["conn-1".to_string()]
);
assert_eq!(
client.resolve_peer_connection_ids("conn-1").await,
vec!["conn-1".to_string()]
);
assert_eq!(
client
.resolve_peer_connection_ids("node1-transientroutenode")
.await,
vec!["conn-1".to_string()],
"a transport-specific route id that shares the peer node should resolve through the core alias index",
);
client
.connection_manager
.upsert_pending("node1-transientroutenode".to_string(), None, None, None)
.await;
assert_eq!(
client
.resolve_peer_connection_ids("node1-transientroutenode")
.await,
vec!["node1-transientroutenode".to_string(), "conn-1".to_string()],
"a pending transient route record must not hide the canonical peer aliases",
);
client
.connection_manager
.upsert_pending(
"desktopnode-browsernode".to_string(),
Some("desktopnode".to_string()),
None,
Some("desktopnode".to_string()),
)
.await;
assert_eq!(
client
.resolve_peer_connection_ids("transientroute-browsernode")
.await,
vec!["desktopnode-browsernode".to_string()],
"compound route ids should resolve canonical peers by shared deterministic id halves even when node indexes are incomplete",
);
}
#[cfg(all(
not(target_arch = "wasm32"),
any(feature = "transport-webrtc", feature = "transport-moq")
))]
#[tokio::test]
async fn resolve_transport_connection_ids_preserves_exact_route_id_before_aliases() {
let client = Client::new_with_app_tag(
crate::test_constants::TEST_PROJECT_ID.to_string(),
"test-app".to_string(),
Box::new(|| None),
);
client
.connection_manager
.upsert_pending(
"canonical-conn".to_string(),
Some("remotepeer".to_string()),
Some("device-hint-1".to_string()),
Some("remotepeer".to_string()),
)
.await;
client
.connection_manager
.set_connecting("canonical-conn")
.await;
assert_eq!(
client
.resolve_transport_connection_ids("transientroute-remotepeer")
.await,
vec![
"transientroute-remotepeer".to_string(),
"canonical-conn".to_string()
],
"transport lookup must check the exact session key before canonical peer aliases"
);
}
#[cfg(all(
not(target_arch = "wasm32"),
any(feature = "transport-webrtc", feature = "transport-moq")
))]
#[tokio::test]
async fn resolve_transport_connection_ids_expands_node_id_to_managed_record() {
let client = Client::new_with_app_tag(
crate::test_constants::TEST_PROJECT_ID.to_string(),
"test-app".to_string(),
Box::new(|| None),
);
client
.connection_manager
.upsert_pending(
"localnode-remotenode".to_string(),
Some("remotenode".to_string()),
Some("remote-device".to_string()),
Some("remotenode".to_string()),
)
.await;
client
.connection_manager
.set_connected("localnode-remotenode", Some("remotenode".to_string()))
.await;
assert_eq!(
client.resolve_transport_connection_ids("remotenode").await,
vec!["remotenode".to_string(), "localnode-remotenode".to_string()],
"transport lookup must not stop at a node-id candidate before finding the managed connection record"
);
}
#[tokio::test]
async fn resolve_peer_connection_records_returns_sorted_records() {
let client = Client::new_with_app_tag(
crate::test_constants::TEST_PROJECT_ID.to_string(),
"test-app".to_string(),
Box::new(|| None),
);
client
.connection_manager
.upsert_pending(
"conn-1".to_string(),
Some("node-1".to_string()),
Some("device-1".to_string()),
Some("node-1".to_string()),
)
.await;
client.connection_manager.set_connecting("conn-1").await;
tokio::time::sleep(std::time::Duration::from_millis(2)).await;
client
.connection_manager
.upsert_pending(
"conn-2".to_string(),
Some("node-2".to_string()),
Some("device-1".to_string()),
Some("node-2".to_string()),
)
.await;
client
.connection_manager
.set_connected("conn-2", Some("node-2".to_string()))
.await;
let records = client.resolve_peer_connection_records("device-1").await;
assert_eq!(records.len(), 2);
assert_eq!(records[0].connection_id, "conn-2");
assert_eq!(records[1].connection_id, "conn-1");
}
#[tokio::test]
async fn managed_connection_device_hint_prefers_authoritative_identity() {
let client = Client::new_with_app_tag(
crate::test_constants::TEST_PROJECT_ID.to_string(),
"test-app".to_string(),
Box::new(|| None),
);
client
.connection_manager
.upsert_pending(
"conn-1".to_string(),
Some("node-1".to_string()),
Some("device-hint-1".to_string()),
Some("node-1".to_string()),
)
.await;
client
.connection_manager
.set_device_id("conn-1", "device-1".to_string())
.await;
assert_eq!(
client
.managed_connection_device_hint("conn-1")
.await
.as_deref(),
Some("device-1")
);
assert_eq!(
client
.managed_connection_device_hint("device-hint-1")
.await
.as_deref(),
Some("device-1")
);
}
#[tokio::test]
async fn managed_connection_device_hint_does_not_borrow_sibling_peer_identity() {
let client = Client::new_with_app_tag(
crate::test_constants::TEST_PROJECT_ID.to_string(),
"test-app".to_string(),
Box::new(|| None),
);
let local_node_id = "local-node";
let desktop_connection_id = format!("{local_node_id}-desktop-node");
let web_connection_id = format!("{local_node_id}-web-node");
client
.connection_manager
.upsert_pending(
desktop_connection_id.clone(),
Some("desktop-node".to_string()),
Some("desktop-device".to_string()),
Some("desktop-node".to_string()),
)
.await;
client
.connection_manager
.set_device_id(&desktop_connection_id, "desktop-device".to_string())
.await;
tokio::time::sleep(std::time::Duration::from_millis(2)).await;
client
.connection_manager
.upsert_pending(
web_connection_id,
Some("web-node".to_string()),
Some("web-device".to_string()),
Some("web-node".to_string()),
)
.await;
assert_eq!(
client
.managed_connection_device_hint(&desktop_connection_id)
.await
.as_deref(),
Some("desktop-device"),
"an exact connection must retain its own authoritative device identity"
);
}
#[tokio::test]
async fn native_accept_promotion_requires_token_when_registry_is_active() {
let client = Client::new_with_app_tag(
crate::test_constants::TEST_PROJECT_ID.to_string(),
"test-app".to_string(),
Box::new(|| None),
);
let connection_id = "conn-native-accept-1";
let remote_node_id = "node-native-accept-1";
let device_id = "device-native-accept-1";
client
.connection_manager
.upsert_pending(
connection_id.to_string(),
Some(remote_node_id.to_string()),
Some(device_id.to_string()),
Some(remote_node_id.to_string()),
)
.await;
client
.connection_manager
.set_connected_with_transport(
connection_id,
Some(remote_node_id.to_string()),
Some(41),
Some("native-accept".to_string()),
)
.await
.expect("connected record");
client.register_session_token(
"native-accept-token".to_string(),
"user-device".to_string(),
0,
);
establish_current_managed_user_device_route(&client, connection_id, 41);
let pending_promotion = client
.promote_known_native_user_device_connection(connection_id, remote_node_id)
.await;
assert_eq!(
pending_promotion, None,
"trusted-device discovery must not bypass an active token gate"
);
client
.validate_session_token_for_connection("native-accept-token", connection_id)
.await
.expect("registered user-device token should admit the connection");
let promoted = client
.promote_known_native_user_device_connection(connection_id, remote_node_id)
.await;
assert_eq!(promoted.as_deref(), Some(device_id));
assert!(matches!(
client.session_admission(connection_id),
crate::session_token::SessionAdmission::Accepted {
scope: Some(ref scope),
authoritative_device_id: Some(ref authoritative),
..
} if scope.as_str() == "user-device" && authoritative == device_id
));
let record = client
.connection_manager
.get_by_connection_id(connection_id)
.await
.expect("managed record");
let adoption = client
.managed_connection_adoption(&record)
.await
.expect("known accepted user-device should be adoptable");
assert_eq!(adoption.device_id.as_deref(), Some(device_id));
assert!(adoption.main_stream_ready);
assert!(
!client
.mark_trusted_user_device_connection_admitted(
connection_id,
"different-authoritative-device",
)
.await,
"an admitted connection must not be rebound to another device"
);
assert!(matches!(
client.session_admission(connection_id),
crate::session_token::SessionAdmission::Accepted {
authoritative_device_id: Some(ref authoritative),
..
} if authoritative == device_id
));
}
#[tokio::test]
async fn managed_connection_bridge_action_requests_rebind_for_stale_transport() {
let client = Client::new_with_app_tag(
crate::test_constants::TEST_PROJECT_ID.to_string(),
"test-app".to_string(),
Box::new(|| None),
);
let node_id =
"5d388fcaaa61efa536d78051e060928f6c1fff0983a32f6b532176b12328c3ac".to_string();
client
.connection_manager
.upsert_pending(
"conn-1".to_string(),
Some(node_id.clone()),
Some("device-1".to_string()),
Some(node_id.clone()),
)
.await;
client
.connection_manager
.set_connected("conn-1", Some(node_id.clone()))
.await;
assert_eq!(
client.managed_connection_bridge_action("conn-1").await,
Some(ManagedConnectionBridgeAction::Rebind {
remote_node_id: Some(node_id),
transport_generation: 0,
})
);
}
#[tokio::test]
async fn transport_connected_peer_session_stays_connecting_until_settled() {
let client = Client::new_with_app_tag(
crate::test_constants::TEST_PROJECT_ID.to_string(),
"test-app".to_string(),
Box::new(|| None),
);
client
.connection_manager
.upsert_pending(
"conn-transport-only-1".to_string(),
Some("node-transport-only-1".to_string()),
Some("device-transport-only-1".to_string()),
Some("node-transport-only-1".to_string()),
)
.await;
client
.connection_manager
.set_connected(
"conn-transport-only-1",
Some("node-transport-only-1".to_string()),
)
.await;
let session = client
.peer_session("device-transport-only-1")
.await
.expect("peer session snapshot");
assert_eq!(session.peer_id, "device-transport-only-1");
assert_eq!(session.status, ConnectionState::Connecting);
assert_eq!(session.health, ConnectionHealth::Unknown);
assert!(!session.settled_ready);
assert_eq!(
session.active_connection_id.as_deref(),
Some("conn-transport-only-1")
);
let connection_state = client
.connection_state("conn-transport-only-1")
.await
.expect("connection state snapshot");
assert_eq!(connection_state.state, "connecting");
assert_eq!(connection_state.transport_state, "connected");
assert_eq!(connection_state.protocol_state, "transport-only");
assert!(!connection_state.routable);
let remote_device = Device {
app_tag: Some("test-app".to_string()),
device_id: "device-transport-only-1".to_string(),
user_id: Some("user-1".to_string()),
device_name: "Remote Device".to_string(),
platform_type: Some("web".to_string()),
capabilities: None,
session_id: None,
node_id: Some("node-transport-only-1".to_string()),
online: true,
ticket: Some("ticket-transport-only-1".to_string()),
tag: Some("test-app".to_string()),
kind: Some("device".to_string()),
metadata: None,
last_seen_at: None,
expires_at: None,
created_at: None,
updated_at: None,
excluded_peers: vec![],
};
let snapshots = merge_device_status_snapshots(
vec![remote_device],
client.connection_manager.list_peer_snapshots().await,
);
let snapshot = snapshots.first().expect("device status snapshot");
assert_eq!(
snapshot.connection_status,
DeviceConnectionStatus::Connecting
);
assert!(!snapshot.settled_ready);
}
#[tokio::test]
async fn unknown_health_is_not_settled_even_with_a_live_transport_generation() {
let client = Client::new_with_app_tag(
crate::test_constants::TEST_PROJECT_ID.to_string(),
"test-app".to_string(),
Box::new(|| None),
);
client
.connection_manager
.upsert_pending(
"conn-unknown-generation".to_string(),
Some("node-unknown-generation".to_string()),
Some("device-unknown-generation".to_string()),
Some("node-unknown-generation".to_string()),
)
.await;
client
.connection_manager
.set_connected_with_transport(
"conn-unknown-generation",
Some("node-unknown-generation".to_string()),
Some(77),
Some("accepted".to_string()),
)
.await
.expect("connected transport");
let session = client
.peer_session("device-unknown-generation")
.await
.expect("peer session");
assert_eq!(session.health, ConnectionHealth::Unknown);
assert!(!session.settled_ready);
assert_eq!(session.readiness_state, ReadinessState::Settling);
let state = client
.connection_state("conn-unknown-generation")
.await
.expect("connection state");
assert!(!state.routable);
}
#[tokio::test]
async fn managed_connection_bridge_action_retires_after_settle_timeout() {
let client = Client::new_with_app_tag(
crate::test_constants::TEST_PROJECT_ID.to_string(),
"test-app".to_string(),
Box::new(|| None),
);
let node_id =
"5d388fcaaa61efa536d78051e060928f6c1fff0983a32f6b532176b12328c3ac".to_string();
client
.connection_manager
.upsert_pending(
"conn-timeout-1".to_string(),
Some(node_id.clone()),
Some("device-timeout-1".to_string()),
Some(node_id.clone()),
)
.await;
client
.connection_manager
.set_connected("conn-timeout-1", Some(node_id))
.await;
client
.connection_manager
.set_last_transport_change_at_ms_for_test(
"conn-timeout-1",
crate::firebase::now_millis_u64()
.min(i64::MAX as u64)
.saturating_sub((MANAGED_SETTLE_DEADLINE_MS + 1) as u64) as i64,
)
.await
.expect("managed record");
let snapshot = client
.managed_connection_health("conn-timeout-1")
.await
.expect("managed connection health");
assert_eq!(snapshot.status, ManagedConnectionHealthStatus::Dead);
assert!(!snapshot.settled_ready);
assert_eq!(
client
.managed_connection_bridge_action("conn-timeout-1")
.await,
Some(ManagedConnectionBridgeAction::Retire {
reason: "settle-timeout".to_string(),
})
);
}
#[tokio::test]
async fn managed_connection_bridge_action_keeps_recovered_transport_after_settle_deadline() {
let client_a = Client::new_with_app_tag(
crate::test_constants::TEST_PROJECT_ID.to_string(),
"deadline-a".to_string(),
Box::new(|| None),
);
let client_b = Client::new_with_app_tag(
crate::test_constants::TEST_PROJECT_ID.to_string(),
"deadline-b".to_string(),
Box::new(|| None),
);
let endpoint_a = Endpoint::builder(iroh::endpoint::presets::N0)
.alpns(vec![crate::native_node::PlutoniumProtocol::ALPN.to_vec()])
.relay_mode(RelayMode::Disabled)
.bind_addr(
"127.0.0.1:0"
.parse::<std::net::SocketAddr>()
.expect("socket addr"),
)
.expect("bind addr a")
.bind()
.await
.expect("bind endpoint a");
let endpoint_b = Endpoint::builder(iroh::endpoint::presets::N0)
.alpns(vec![crate::native_node::PlutoniumProtocol::ALPN.to_vec()])
.relay_mode(RelayMode::Disabled)
.bind_addr(
"127.0.0.1:0"
.parse::<std::net::SocketAddr>()
.expect("socket addr"),
)
.expect("bind addr b")
.bind()
.await
.expect("bind endpoint b");
client_a.adopt_endpoint(endpoint_a).await;
client_b.adopt_endpoint(endpoint_b).await;
let endpoint_a = client_a.get_endpoint().await.expect("endpoint a");
let endpoint_b = client_b.get_endpoint().await.expect("endpoint b");
let endpoint_a_id = endpoint_a.id();
let endpoint_b_id = endpoint_b.id();
let endpoint_b_addr = wait_for_endpoint_addr(&endpoint_b, 5_000)
.await
.expect("endpoint b addr");
client_a
.ensure_connected_addr(endpoint_b_id, endpoint_b_addr)
.await
.expect("initial connection");
let kept_connection = tokio::time::timeout(Duration::from_secs(5), async {
loop {
if let Some(connection) = client_b.get_connection(endpoint_a_id).await {
break connection;
}
sleep(Duration::from_millis(50)).await;
}
})
.await
.expect("timed out waiting for kept connection");
let kept_stable_id = kept_connection.stable_id() as u64;
let remote_node_id = endpoint_a_id.to_string();
let local_node_id = endpoint_b_id.to_string();
let connection_id = Client::deterministic_connection_id(&local_node_id, &remote_node_id);
let synthetic_closed_stable_id = kept_stable_id.saturating_add(100);
client_b
.connection_manager
.upsert_pending(
connection_id.clone(),
Some(remote_node_id.clone()),
Some("device-a".to_string()),
Some(remote_node_id.clone()),
)
.await;
client_b
.connection_manager
.set_connected_with_transport(
&connection_id,
Some(remote_node_id.clone()),
Some(synthetic_closed_stable_id),
Some("incoming".to_string()),
)
.await
.expect("connected record");
client_b
.connection_manager
.set_device_id(&connection_id, "device-a".to_string())
.await
.expect("device binding");
client_b.session_token_registry.mark_accepted(
&connection_id,
crate::session_token::SessionAdmissionMechanism::SessionToken,
Some(crate::session_token::GrantScope::from("user-device")),
None,
);
client_b.bind_inbound_native_admission_stream_contract(
&connection_id,
crate::native_protocol::SessionTokenStreamContract::PersistentControl,
);
client_b.bind_outbound_native_admission_stream_contract(
&connection_id,
crate::native_protocol::SessionTokenStreamContract::PersistentControl,
);
client_b.bind_inbound_session_token_transport(&connection_id, kept_stable_id);
client_b
.remote_session_admission_proofs
.write()
.expect("remote proof registry")
.insert(
connection_id.clone(),
RemoteSessionAdmissionProof {
transport_stable_id: kept_stable_id,
token_fingerprint: "kept-route-proof".to_string(),
approval_scope: "user-device".to_string(),
},
);
assert!(client_b
.native_admission_route_is_ready_for_transport(&connection_id, Some(kept_stable_id),));
let resolution = client_b
.reconcile_incoming_transport_after_local_close(
&connection_id,
endpoint_a_id,
&remote_node_id,
synthetic_closed_stable_id,
"LocallyClosed",
)
.await;
assert_eq!(
resolution,
IncomingTransportCloseResolution::PreservedKeptTransport
);
let settled = client_b
.wait_for_settled_peer("device-a", Some(2_000))
.await
.expect("settled peer snapshot");
assert!(
settled.settled_ready,
"expected recovered peer to be settled"
);
assert_eq!(
settled.active_connection_id.as_deref(),
Some(connection_id.as_str())
);
client_b
.connection_manager
.set_last_transport_change_at_ms_for_test(
&connection_id,
crate::firebase::now_millis_u64()
.min(i64::MAX as u64)
.saturating_sub((MANAGED_SETTLE_DEADLINE_MS + 1) as u64) as i64,
)
.await
.expect("managed record");
let snapshot = client_b
.managed_connection_health(&connection_id)
.await
.expect("managed connection health");
assert_eq!(snapshot.status, ManagedConnectionHealthStatus::Healthy);
assert!(snapshot.settled_ready);
assert_eq!(snapshot.active_transport_stable_id, Some(kept_stable_id));
assert_eq!(
client_b
.managed_connection_bridge_action(&connection_id)
.await,
Some(ManagedConnectionBridgeAction::Healthy)
);
}
#[test]
fn duplicate_closed_handling_preserves_any_healthy_replacement_transport() {
assert_eq!(
duplicate_closed_handling(
"ApplicationClosed(0, b\"duplicate-kept-existing\")",
true,
true,
2,
1
),
DuplicateClosedHandling::RebindKeptTransport
);
assert_eq!(
duplicate_closed_handling("LocallyClosed", true, true, 2, 1),
DuplicateClosedHandling::RebindKeptTransport
);
assert_eq!(
duplicate_closed_handling(
"ApplicationClosed(0, b\"replaced-by-fresh-incoming\")",
true,
true,
2,
1
),
DuplicateClosedHandling::RebindKeptTransport
);
assert_eq!(
duplicate_closed_handling(
"ApplicationClosed(0, b\"duplicate-kept-existing\")",
true,
false,
2,
1
),
DuplicateClosedHandling::RebindKeptTransport
);
assert_eq!(
duplicate_closed_handling(
"ApplicationClosed(0, b\"duplicate-kept-existing\")",
true,
true,
1,
1
),
DuplicateClosedHandling::RetireClosedTransport
);
}
#[test]
fn incoming_local_timeout_close_waits_for_replacement_grace() {
assert!(incoming_local_close_should_wait_for_replacement("TimedOut"));
assert!(incoming_local_close_should_wait_for_replacement(
"Some(TimedOut)"
));
assert!(incoming_local_close_should_wait_for_replacement(
"transport timed out while replacement was active"
));
assert!(incoming_local_close_should_wait_for_replacement(
"ApplicationClosed(0, b\"replaced-by-new-outbound\")"
));
assert!(!incoming_local_close_should_wait_for_replacement(
"ApplicationClosed(0, b\"manual disconnect\")"
));
assert!(!incoming_local_close_should_wait_for_replacement("Closed"));
}
#[tokio::test]
async fn incoming_local_timeout_close_preserves_manager_record_for_replacement_grace() {
let client = Client::new_with_app_tag(
crate::test_constants::TEST_PROJECT_ID.to_string(),
"test-app".to_string(),
Box::new(|| None),
);
client
.connection_manager
.upsert_pending(
"conn-timeout-close".to_string(),
Some("node-timeout-close".to_string()),
Some("device-timeout-close".to_string()),
Some("node-timeout-close".to_string()),
)
.await;
client
.connection_manager
.set_connected_with_transport(
"conn-timeout-close",
Some("node-timeout-close".to_string()),
Some(10),
Some("incoming".to_string()),
)
.await
.expect("connected record");
let endpoint_id = "5d388fcaaa61efa536d78051e060928f6c1fff0983a32f6b532176b12328c3ac"
.parse()
.expect("endpoint id");
let resolution = client
.reconcile_incoming_transport_after_local_close(
"conn-timeout-close",
endpoint_id,
"node-timeout-close",
10,
"Some(TimedOut)",
)
.await;
assert_eq!(
resolution,
IncomingTransportCloseResolution::PreservedKeptTransport
);
let record = client
.connection_manager
.get_by_connection_id("conn-timeout-close")
.await
.expect("record");
assert_eq!(
record.status_reason.as_deref(),
Some(crate::lifecycle_reason::REASON_REPLACEMENT_IN_PROGRESS)
);
assert!(matches!(
record.state,
ConnectionState::Connecting | ConnectionState::Pending | ConnectionState::Connected
));
}
#[cfg(not(target_arch = "wasm32"))]
#[tokio::test]
async fn native_readiness_confirmation_emits_the_canonical_snapshot() {
let client = Client::new_with_app_tag(
crate::test_constants::TEST_PROJECT_ID.to_string(),
"test-app".to_string(),
Box::new(|| None),
);
let mut updates = client.subscribe_native_connection_state_updates();
client
.connection_manager
.upsert_pending(
"conn-readiness-event".to_string(),
Some("invalid-node-id".to_string()),
Some("device-readiness-event".to_string()),
Some("invalid-node-id".to_string()),
)
.await;
client
.connection_manager
.set_connected_with_transport(
"conn-readiness-event",
Some("invalid-node-id".to_string()),
Some(42),
Some("incoming".to_string()),
)
.await
.expect("connected record");
assert!(
!client
.confirm_managed_connection_readiness("conn-readiness-event")
.await
);
let update = tokio::time::timeout(std::time::Duration::from_secs(1), updates.recv())
.await
.expect("readiness update timeout")
.expect("readiness update");
assert_eq!(update.connection_id, "conn-readiness-event");
assert!(!update.routable);
}
#[cfg(not(target_arch = "wasm32"))]
#[tokio::test]
async fn native_transport_proof_settles_only_the_exact_current_generation() {
let client = Client::new_with_app_tag(
crate::test_constants::TEST_PROJECT_ID.to_string(),
"test-app".to_string(),
Box::new(|| None),
);
client
.connection_manager
.upsert_pending(
"conn-transport-proof".to_string(),
Some("node-transport-proof".to_string()),
Some("device-transport-proof".to_string()),
Some("node-transport-proof".to_string()),
)
.await;
client
.connection_manager
.set_connected_with_transport(
"conn-transport-proof",
Some("node-transport-proof".to_string()),
Some(42),
Some("incoming".to_string()),
)
.await
.expect("connected record");
client
.connection_manager
.mark_transport_replaced(
"conn-transport-proof",
Some(43),
Some("incoming".to_string()),
Some(crate::lifecycle_reason::REASON_REPLACEMENT_IN_PROGRESS.to_string()),
)
.await
.expect("replacement record");
assert!(
!client
.confirm_managed_connection_readiness_from_transport_proof(
"conn-transport-proof",
42,
)
.await
);
let stale_generation = client
.connection_manager
.get_by_connection_id("conn-transport-proof")
.await
.expect("current record");
assert_eq!(
stale_generation.status_reason.as_deref(),
Some(crate::lifecycle_reason::REASON_REPLACEMENT_IN_PROGRESS)
);
assert!(
client
.confirm_managed_connection_readiness_from_transport_proof(
"conn-transport-proof",
43,
)
.await
);
let settled = client
.connection_manager
.peer_snapshot("conn-transport-proof")
.await
.expect("settled peer");
assert_eq!(settled.health, ConnectionHealth::Healthy);
let settled_record = client
.connection_manager
.get_by_connection_id("conn-transport-proof")
.await
.expect("settled record");
assert_eq!(settled_record.status_reason, None);
}
#[cfg(not(target_arch = "wasm32"))]
#[tokio::test]
async fn native_retirement_emits_authoritative_terminal_snapshot_before_removal() {
let client = Client::new_with_app_tag(
crate::test_constants::TEST_PROJECT_ID.to_string(),
"test-app".to_string(),
Box::new(|| None),
);
let mut updates = client.subscribe_native_connection_state_updates();
client
.connection_manager
.upsert_pending(
"conn-terminal".to_string(),
Some("node-remote".to_string()),
Some("device-remote".to_string()),
Some("node-remote".to_string()),
)
.await;
client
.connection_manager
.set_connected_with_transport(
"conn-terminal",
Some("node-remote".to_string()),
Some(42),
Some("incoming".to_string()),
)
.await
.expect("connected record");
client.session_token_registry.mark_accepted(
"conn-terminal",
crate::session_token::SessionAdmissionMechanism::SessionToken,
Some(crate::session_token::GrantScope::from("user-device")),
Some("device-remote".to_string()),
);
client.bind_inbound_native_admission_stream_contract(
"conn-terminal",
crate::native_protocol::SessionTokenStreamContract::PersistentControl,
);
client.bind_outbound_native_admission_stream_contract(
"conn-terminal",
crate::native_protocol::SessionTokenStreamContract::PersistentControl,
);
client.bind_inbound_session_token_transport("conn-terminal", 42);
client
.remote_session_admission_proofs
.write()
.expect("remote proof registry")
.insert(
"conn-terminal".to_string(),
RemoteSessionAdmissionProof {
transport_stable_id: 42,
token_fingerprint: "terminal-token".to_string(),
approval_scope: "user-device".to_string(),
},
);
client.set_connection_application_crypto_required("conn-terminal");
client.set_connection_application_crypto_key("conn-terminal", [7u8; 32]);
client.confirm_connection_application_crypto("conn-terminal");
client
.retire_managed_connection(
"conn-terminal",
Some(crate::lifecycle_reason::REASON_MANUAL_DISCONNECT.to_string()),
)
.await;
let terminal = tokio::time::timeout(std::time::Duration::from_secs(1), updates.recv())
.await
.expect("terminal update timeout")
.expect("terminal update");
assert_eq!(terminal.connection_id, "conn-terminal");
assert_eq!(terminal.state, "closed");
assert!(!terminal.routable);
assert_eq!(
terminal.last_disconnect_reason.as_deref(),
Some(crate::lifecycle_reason::REASON_MANUAL_DISCONNECT),
);
assert!(
client
.connection_manager
.get_by_connection_id("conn-terminal")
.await
.is_none(),
"terminal snapshot must be emitted before the core record is removed",
);
assert!(matches!(
client.session_admission("conn-terminal"),
crate::session_token::SessionAdmission::Pending,
));
assert_eq!(
client.native_admission_stream_contracts("conn-terminal"),
crate::client::NativeAdmissionStreamContracts::default(),
);
assert!(!client.inbound_session_token_admitted_for_transport("conn-terminal", 42));
assert!(!client.remote_session_token_admitted_for_transport("conn-terminal", 42));
assert!(client
.connection_application_crypto_key("conn-terminal")
.is_none());
assert!(!client.connection_requires_application_crypto("conn-terminal"));
assert!(!client.connection_application_crypto_is_confirmed("conn-terminal"));
assert!(!client
.connection_application_key_agreements
.read()
.expect("application key agreement registry")
.contains_key("conn-terminal"));
}
#[cfg(not(target_arch = "wasm32"))]
#[tokio::test]
async fn native_transient_retirement_preserves_logical_session_admission() {
let client = Client::new_with_app_tag(
crate::test_constants::TEST_PROJECT_ID.to_string(),
"test-app".to_string(),
Box::new(|| None),
);
let connection_id = "conn-transient-retirement";
client
.connection_manager
.upsert_pending(
connection_id.to_string(),
Some("node-remote".to_string()),
Some("device-remote".to_string()),
Some("node-remote".to_string()),
)
.await;
client.session_token_registry.mark_accepted(
connection_id,
crate::session_token::SessionAdmissionMechanism::SessionToken,
Some(crate::session_token::GrantScope::from("user-device")),
Some("device-remote".to_string()),
);
client.bind_outbound_native_admission_stream_contract(
connection_id,
crate::native_protocol::SessionTokenStreamContract::PersistentControl,
);
client
.remote_session_admission_proofs
.write()
.expect("remote proof registry")
.insert(
connection_id.to_string(),
RemoteSessionAdmissionProof {
transport_stable_id: 42,
token_fingerprint: "replacement-token".to_string(),
approval_scope: "user-device".to_string(),
},
);
client.set_connection_application_crypto_required(connection_id);
client.set_connection_application_crypto_key(connection_id, [9u8; 32]);
client.confirm_connection_application_crypto(connection_id);
client
.retire_managed_connection(
connection_id,
Some(crate::lifecycle_reason::REASON_REPLACEMENT_IN_PROGRESS.to_string()),
)
.await;
assert!(matches!(
client.session_admission(connection_id),
crate::session_token::SessionAdmission::Accepted { .. },
));
assert_eq!(
client
.native_admission_stream_contracts(connection_id)
.outbound,
Some(crate::native_protocol::SessionTokenStreamContract::PersistentControl),
);
assert!(client.remote_session_token_admitted_for_transport(connection_id, 42));
assert_eq!(
client.connection_application_crypto_key(connection_id),
Some([9u8; 32]),
"transient route replacement must preserve the logical application session",
);
assert!(client.connection_requires_application_crypto(connection_id));
assert!(client.connection_application_crypto_is_confirmed(connection_id));
}
#[cfg(all(not(target_arch = "wasm32"), feature = "transport-webrtc"))]
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn retire_managed_connection_defers_while_native_webrtc_is_connecting() {
let client = Client::new_with_app_tag(
crate::test_constants::TEST_PROJECT_ID.to_string(),
"test-app".to_string(),
Box::new(|| None),
);
client
.connection_manager
.upsert_pending(
"conn-webrtc".to_string(),
Some("node-remote".to_string()),
Some("device-remote".to_string()),
Some("node-remote".to_string()),
)
.await;
client
.connection_manager
.set_connected_with_transport(
"conn-webrtc",
Some("node-remote".to_string()),
None,
Some("incoming".to_string()),
)
.await
.expect("connected record");
let session = std::sync::Arc::new(
crate::transport::NativeWebRTCDataChannel::new(
"node-z",
"node-a",
&crate::client::WebRTCConfig {
ice_servers: vec![],
privacy_mode: false,
lan_mode: false,
},
std::sync::Arc::new(|_| Box::pin(async move {})),
"negotiation-test",
)
.await
.expect("native webrtc session"),
);
session.start().await.expect("session started");
client
.native_webrtc_sessions
.write()
.await
.insert("conn-webrtc".to_string(), session.clone());
client
.retire_managed_connection("conn-webrtc", Some("transport-replaced".to_string()))
.await;
assert!(
client
.connection_manager
.get_by_connection_id("conn-webrtc")
.await
.is_some(),
"connection record should remain while webrtc is still connecting",
);
assert!(
client
.native_webrtc_sessions
.read()
.await
.contains_key("conn-webrtc"),
"webrtc session should remain registered while cleanup is deferred",
);
assert_eq!(
client
.deferred_managed_retirements
.read()
.await
.get("conn-webrtc")
.cloned(),
Some(Some("transport-replaced".to_string())),
);
session.close();
client
.native_webrtc_sessions
.write()
.await
.remove("conn-webrtc");
assert!(
client
.finalize_deferred_managed_retirement_if_needed("conn-webrtc")
.await,
"deferred retirement should finalize after the session disappears",
);
assert!(
client
.connection_manager
.get_by_connection_id("conn-webrtc")
.await
.is_none(),
"finalized deferred retirement should remove the connection record",
);
}
#[cfg(all(not(target_arch = "wasm32"), feature = "transport-webrtc"))]
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn product_webrtc_lookup_exposes_only_live_sessions() {
let client = Client::new_with_app_tag(
crate::test_constants::TEST_PROJECT_ID.to_string(),
"test-app".to_string(),
Box::new(|| None),
);
client
.connection_manager
.upsert_pending(
"conn-product-visibility".to_string(),
Some("node-a".to_string()),
Some("device-a".to_string()),
Some("node-a".to_string()),
)
.await;
client
.connection_manager
.set_connected_with_transport(
"conn-product-visibility",
Some("node-a".to_string()),
Some(1),
Some("incoming".to_string()),
)
.await
.expect("connected record");
let session = std::sync::Arc::new(
crate::transport::NativeWebRTCDataChannel::new(
"node-z",
"node-a",
&crate::client::WebRTCConfig {
ice_servers: vec![],
privacy_mode: false,
lan_mode: false,
},
std::sync::Arc::new(|_| Box::pin(async move {})),
"negotiation-product-visibility",
)
.await
.expect("native webrtc session"),
);
client
.native_webrtc_sessions
.write()
.await
.insert("conn-product-visibility".to_string(), session.clone());
client
.install_native_optional_route_generation(
"conn-product-visibility",
crate::route_policy::KnownRoute::WebRtc,
std::sync::Arc::as_ptr(&session) as usize,
)
.await
.expect("current WebRTC route generation");
assert!(
client
.get_webrtc_session_for_peer("conn-product-visibility")
.await
.is_none(),
"idle sessions must not be exposed to product handlers",
);
session.start().await.expect("session started");
let exposed = client
.get_webrtc_session_for_peer("conn-product-visibility")
.await
.expect("connecting session should be exposed");
assert!(std::sync::Arc::ptr_eq(&exposed, &session));
session.close();
assert!(
client
.get_webrtc_session_for_peer("conn-product-visibility")
.await
.is_none(),
"closed sessions must not be exposed to product handlers",
);
}
#[cfg(all(not(target_arch = "wasm32"), feature = "transport-webrtc"))]
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn retired_webrtc_generation_cannot_emit_peer_data() {
let client = Client::new_with_app_tag(
crate::test_constants::TEST_PROJECT_ID.to_string(),
"test-app".to_string(),
Box::new(|| None),
);
let config = crate::client::WebRTCConfig {
ice_servers: vec![],
privacy_mode: false,
lan_mode: false,
};
let signal_sender: crate::transport::NativeWebRTCSignalSender =
std::sync::Arc::new(|_| Box::pin(async move {}));
let retired = std::sync::Arc::new(
crate::transport::NativeWebRTCDataChannel::new(
"node-z",
"node-a",
&config,
signal_sender.clone(),
"negotiation-retired",
)
.await
.expect("retired session"),
);
let current = std::sync::Arc::new(
crate::transport::NativeWebRTCDataChannel::new(
"node-z",
"node-a",
&config,
signal_sender,
"negotiation-current",
)
.await
.expect("current session"),
);
client
.native_webrtc_sessions
.write()
.await
.insert("conn-generation-fence".to_string(), current.clone());
client
.connection_manager
.upsert_pending(
"conn-generation-fence".to_string(),
Some("node-a".to_string()),
None,
Some("node-a".to_string()),
)
.await;
client
.connection_manager
.set_connected_with_transport(
"conn-generation-fence",
None,
Some(41),
Some(crate::transport_label::IROH_RELAY.to_string()),
)
.await;
current.force_state_for_test(crate::transport::NativeWebRTCState::Connected);
client
.native_optional_route_generations
.write()
.await
.insert(
"conn-generation-fence".to_string(),
optional_route_generations(
crate::route_policy::KnownRoute::WebRtc,
crate::client::NativeOptionalRouteGeneration {
transport_stable_id: 41,
transport_generation: 1,
route_generation: 0,
route_instance_id: std::sync::Arc::as_ptr(¤t) as usize,
},
),
);
let mut events = client.subscribe_native_peer_data();
assert!(!client
.handle_current_native_webrtc_peer_data(
"conn-generation-fence",
Some("node-a"),
&retired,
bytes::Bytes::from_static(b"retired"),
)
.await
.expect("retired callback should be ignored"));
assert!(
tokio::time::timeout(Duration::from_millis(25), events.recv())
.await
.is_err()
);
assert!(client
.handle_current_native_webrtc_peer_data(
"conn-generation-fence",
Some("node-a"),
¤t,
bytes::Bytes::from_static(b"current"),
)
.await
.expect("current callback should emit"));
let event = tokio::time::timeout(Duration::from_secs(1), events.recv())
.await
.expect("current event timeout")
.expect("current event");
assert_eq!(event.transport, "webrtc");
assert_eq!(event.transport_stable_id, 41);
assert_eq!(event.transport_generation, 1);
assert_eq!(event.route_generation, 0);
assert_eq!(event.payload, b"current");
}
#[cfg(all(not(target_arch = "wasm32"), feature = "transport-moq"))]
#[tokio::test]
async fn retired_moq_generation_cannot_emit_peer_data() {
let client = Client::new_with_app_tag(
crate::test_constants::TEST_PROJECT_ID.to_string(),
"test-app".to_string(),
Box::new(|| None),
);
let config = MoQConfig {
relay_url: "https://relay.invalid/moq".to_string(),
..MoQConfig::default()
};
let retired = std::sync::Arc::new(
crate::transport::NativeMoQSession::new("node-z", "node-a", &config)
.await
.expect("retired session"),
);
let current = std::sync::Arc::new(
crate::transport::NativeMoQSession::new("node-z", "node-a", &config)
.await
.expect("current session"),
);
client
.native_moq_sessions
.write()
.await
.insert("conn-generation-fence".to_string(), current.clone());
client
.connection_manager
.upsert_pending(
"conn-generation-fence".to_string(),
Some("node-a".to_string()),
None,
Some("node-a".to_string()),
)
.await;
client
.connection_manager
.set_connected_with_transport(
"conn-generation-fence",
None,
Some(42),
Some(crate::transport_label::IROH_RELAY.to_string()),
)
.await;
current.force_state_for_test(crate::transport::NativeMoQState::Connected);
client
.native_optional_route_generations
.write()
.await
.insert(
"conn-generation-fence".to_string(),
optional_route_generations(
crate::route_policy::KnownRoute::Moq,
crate::client::NativeOptionalRouteGeneration {
transport_stable_id: 42,
transport_generation: 1,
route_generation: 0,
route_instance_id: std::sync::Arc::as_ptr(¤t) as usize,
},
),
);
let mut events = client.subscribe_native_peer_data();
assert!(!client
.handle_current_native_moq_peer_data(
"conn-generation-fence",
Some("node-a"),
&retired,
bytes::Bytes::from_static(b"retired"),
)
.await
.expect("retired callback should be ignored"));
assert!(
tokio::time::timeout(Duration::from_millis(25), events.recv())
.await
.is_err()
);
assert_eq!(
client
.handle_current_native_moq_terminal_failure(
"conn-generation-fence",
"node-a",
retired.clone(),
)
.await,
crate::client::NativeOptionalRouteTerminalDisposition::Stale,
"a retired route cannot clear or restart the current MoQ route",
);
let installed = client
.native_moq_sessions
.read()
.await
.get("conn-generation-fence")
.cloned()
.expect("current MoQ route remains installed");
assert!(std::sync::Arc::ptr_eq(&installed, ¤t));
assert!(client
.handle_current_native_moq_peer_data(
"conn-generation-fence",
Some("node-a"),
¤t,
bytes::Bytes::from_static(b"current"),
)
.await
.expect("current callback should emit"));
let event = tokio::time::timeout(Duration::from_secs(1), events.recv())
.await
.expect("current event timeout")
.expect("current event");
assert_eq!(event.transport, "moq");
assert_eq!(event.transport_stable_id, 42);
assert_eq!(event.transport_generation, 1);
assert_eq!(event.route_generation, 0);
assert_eq!(event.payload, b"current");
}
#[cfg(all(not(target_arch = "wasm32"), feature = "transport-moq"))]
#[tokio::test]
async fn repeated_current_moq_terminal_callbacks_start_only_one_recovery() {
let client = Client::new_with_app_tag(
crate::test_constants::TEST_PROJECT_ID.to_string(),
"test-app".to_string(),
Box::new(|| None),
);
*client.node_id.write().await = Some("node-z".to_string());
let config = MoQConfig {
relay_url: "https://relay.invalid/moq".to_string(),
..MoQConfig::default()
};
client
.update_transport_config(TransportConfig {
moq: Some(config.clone()),
..TransportConfig::default()
})
.await
.expect("transport config");
let session = std::sync::Arc::new(
crate::transport::NativeMoQSession::new("node-z", "node-a", &config)
.await
.expect("current session"),
);
session.force_state_for_test(crate::transport::NativeMoQState::Connected);
client
.native_moq_sessions
.write()
.await
.insert("conn-moq-terminal".to_string(), session.clone());
client
.connection_manager
.upsert_pending(
"conn-moq-terminal".to_string(),
Some("node-a".to_string()),
Some("device-a".to_string()),
Some("node-a".to_string()),
)
.await;
client
.connection_manager
.set_connected_with_transport(
"conn-moq-terminal",
Some("node-a".to_string()),
Some(43),
Some(crate::transport_label::IROH_RELAY.to_string()),
)
.await
.expect("connected record");
client
.native_optional_route_generations
.write()
.await
.insert(
"conn-moq-terminal".to_string(),
optional_route_generations(
crate::route_policy::KnownRoute::Moq,
crate::client::NativeOptionalRouteGeneration {
transport_stable_id: 43,
transport_generation: 1,
route_generation: 0,
route_instance_id: std::sync::Arc::as_ptr(&session) as usize,
},
),
);
let (first, second) = tokio::join!(
client.handle_current_native_moq_terminal_failure(
"conn-moq-terminal",
"node-a",
session.clone(),
),
client.handle_current_native_moq_terminal_failure(
"conn-moq-terminal",
"node-a",
session.clone(),
),
);
let handled = [first, second]
.into_iter()
.filter(|outcome| {
matches!(
outcome,
crate::client::NativeOptionalRouteTerminalDisposition::Handled { .. }
)
})
.count();
assert_eq!(handled, 1);
assert!([first, second].into_iter().any(|outcome| {
matches!(
outcome,
crate::client::NativeOptionalRouteTerminalDisposition::Handled {
recovery_started: false
}
)
}));
assert!(client
.native_moq_sessions
.read()
.await
.get("conn-moq-terminal")
.is_none());
}
#[test]
fn auto_connect_tie_break_observes_existing_transport_before_waiting() {
assert_eq!(
auto_connect_tie_break_decision("node-a", "node-z", true, 0, 5_000),
AutoConnectTieBreakDecision::ObserveConnectedTransport
);
assert_eq!(
auto_connect_tie_break_decision("node-a", "node-z", false, 500, 5_000),
AutoConnectTieBreakDecision::WaitForInitiator
);
assert_eq!(
auto_connect_tie_break_decision("node-a", "node-z", false, 6_000, 5_000),
AutoConnectTieBreakDecision::ActAsInitiator
);
assert_eq!(
auto_connect_tie_break_decision("node-z", "node-a", false, 0, 5_000),
AutoConnectTieBreakDecision::ActAsInitiator
);
}
#[tokio::test]
async fn managed_connection_health_reports_dead_for_closed_connection() {
let client = Client::new_with_app_tag(
crate::test_constants::TEST_PROJECT_ID.to_string(),
"test-app".to_string(),
Box::new(|| None),
);
client
.connection_manager
.upsert_pending(
"conn-1".to_string(),
Some("node-1".to_string()),
Some("device-1".to_string()),
Some("node-1".to_string()),
)
.await;
client
.connection_manager
.set_closed("conn-1", Some("closed-for-test".to_string()))
.await;
let snapshot = client
.managed_connection_health("conn-1")
.await
.expect("snapshot");
assert_eq!(snapshot.status, ManagedConnectionHealthStatus::Dead);
assert!(!snapshot.settled_ready);
assert_eq!(snapshot.device_id_hint.as_deref(), Some("device-1"));
}
#[tokio::test]
async fn managed_connection_health_reports_replacement_pending_without_successor_transport() {
let client = Client::new_with_app_tag(
crate::test_constants::TEST_PROJECT_ID.to_string(),
"test-app".to_string(),
Box::new(|| None),
);
client
.connection_manager
.upsert_pending(
"conn-replacement-1".to_string(),
Some("node-replacement-1".to_string()),
Some("device-replacement-1".to_string()),
Some("node-replacement-1".to_string()),
)
.await;
client
.connection_manager
.set_connected_with_transport(
"conn-replacement-1",
Some("node-replacement-1".to_string()),
Some(41),
Some("outgoing".to_string()),
)
.await
.expect("connected record");
client
.connection_manager
.mark_transport_replaced(
"conn-replacement-1",
None,
Some("outgoing-closed".to_string()),
Some("replacement-in-progress".to_string()),
)
.await
.expect("replacement record");
let snapshot = client
.managed_connection_health("conn-replacement-1")
.await
.expect("snapshot");
assert_eq!(
snapshot.status,
ManagedConnectionHealthStatus::AwaitingReplacement
);
assert!(snapshot.replacement_pending);
assert_eq!(snapshot.active_transport_stable_id, None);
assert_eq!(
snapshot.readiness_state,
ReadinessState::AwaitingReplacement
);
assert_eq!(
snapshot.readiness_reason,
"missing-active-transport".to_string()
);
}
#[tokio::test]
async fn connection_state_snapshot_reports_connecting_during_in_place_reconnect_close() {
let client = Client::new_with_app_tag(
crate::test_constants::TEST_PROJECT_ID.to_string(),
"test-app".to_string(),
Box::new(|| None),
);
client
.connection_manager
.upsert_pending(
"conn-inplace-reconnect".to_string(),
Some("node-inplace-reconnect".to_string()),
Some("device-inplace-reconnect".to_string()),
Some("node-inplace-reconnect".to_string()),
)
.await;
client
.connection_manager
.set_connected_with_transport(
"conn-inplace-reconnect",
Some("node-inplace-reconnect".to_string()),
Some(41),
Some("outgoing".to_string()),
)
.await
.expect("connected record");
client
.connection_manager
.set_closed(
"conn-inplace-reconnect",
Some(crate::lifecycle_reason::REASON_REPLACEMENT_IN_PROGRESS.to_string()),
)
.await
.expect("closed record");
let snapshot = client
.connection_state("conn-inplace-reconnect")
.await
.expect("connection state snapshot");
assert_eq!(
snapshot.state, "connecting",
"in-place reconnect close must surface as connecting, not closed/disconnected"
);
assert_eq!(snapshot.transport_state, "connecting");
assert_eq!(snapshot.protocol_state, "connecting");
}
#[tokio::test]
async fn connection_state_snapshot_reports_closed_for_genuine_close() {
let client = Client::new_with_app_tag(
crate::test_constants::TEST_PROJECT_ID.to_string(),
"test-app".to_string(),
Box::new(|| None),
);
client
.connection_manager
.upsert_pending(
"conn-genuine-close".to_string(),
Some("node-genuine-close".to_string()),
Some("device-genuine-close".to_string()),
Some("node-genuine-close".to_string()),
)
.await;
client
.connection_manager
.set_connected_with_transport(
"conn-genuine-close",
Some("node-genuine-close".to_string()),
Some(41),
Some("outgoing".to_string()),
)
.await
.expect("connected record");
client
.connection_manager
.set_closed(
"conn-genuine-close",
Some(crate::lifecycle_reason::REASON_MANUAL_DISCONNECT.to_string()),
)
.await
.expect("closed record");
let snapshot = client
.connection_state("conn-genuine-close")
.await
.expect("connection state snapshot");
assert_eq!(snapshot.state, "closed");
assert_eq!(snapshot.transport_state, "closed");
assert_eq!(snapshot.protocol_state, "closed");
}
#[tokio::test]
async fn managed_connection_bridge_action_awaits_replacement_without_successor_transport() {
let client = Client::new_with_app_tag(
crate::test_constants::TEST_PROJECT_ID.to_string(),
"test-app".to_string(),
Box::new(|| None),
);
client
.connection_manager
.upsert_pending(
"conn-replacement-retire-repro".to_string(),
Some("node-replacement-retire-repro".to_string()),
Some("device-replacement-retire-repro".to_string()),
Some("node-replacement-retire-repro".to_string()),
)
.await;
client
.connection_manager
.set_connected_with_transport(
"conn-replacement-retire-repro",
Some("node-replacement-retire-repro".to_string()),
Some(77),
Some("outgoing".to_string()),
)
.await
.expect("connected record");
client
.connection_manager
.mark_transport_replaced(
"conn-replacement-retire-repro",
None,
Some("outgoing-closed".to_string()),
Some("replacement-in-progress".to_string()),
)
.await
.expect("replacement record");
let snapshot = client
.managed_connection_health("conn-replacement-retire-repro")
.await
.expect("snapshot");
assert_eq!(
snapshot.status,
ManagedConnectionHealthStatus::AwaitingReplacement
);
assert!(snapshot.replacement_pending);
assert_eq!(snapshot.active_transport_stable_id, None);
assert_eq!(
client
.managed_connection_bridge_action("conn-replacement-retire-repro")
.await,
Some(ManagedConnectionBridgeAction::AwaitReplacement {
reason: "missing-active-transport".to_string(),
})
);
}
#[tokio::test]
async fn reconfirmed_transport_stable_id_clears_replacement_and_settles_peer_session() {
let client = Client::new_with_app_tag(
crate::test_constants::TEST_PROJECT_ID.to_string(),
"test-app".to_string(),
Box::new(|| None),
);
let connection_id = "conn-reconfirmed-stable-id";
let node_id = "node-reconfirmed-stable-id";
let device_id = "device-reconfirmed-stable-id";
client
.connection_manager
.upsert_pending(
connection_id.to_string(),
Some(node_id.to_string()),
Some(device_id.to_string()),
Some(node_id.to_string()),
)
.await;
client
.connection_manager
.set_connected_with_transport(
connection_id,
Some(node_id.to_string()),
Some(101),
Some("initial-connect".to_string()),
)
.await
.expect("connected record");
client
.connection_manager
.mark_transport_replaced(
connection_id,
None,
Some("transient-disconnect".to_string()),
Some(crate::lifecycle_reason::REASON_REPLACEMENT_IN_PROGRESS.to_string()),
)
.await
.expect("replacement marker");
client
.connection_manager
.set_health(connection_id, ConnectionHealth::Stale)
.await;
let before = client.peer_session(device_id).await.expect("peer session");
assert!(!before.settled_ready);
assert_eq!(before.status, ConnectionState::Connecting);
assert_eq!(before.readiness_state, ReadinessState::TransportOnly);
assert_eq!(
before.error.as_deref(),
Some(crate::lifecycle_reason::REASON_REPLACEMENT_IN_PROGRESS)
);
client
.connection_manager
.set_connected_with_transport(
connection_id,
Some(node_id.to_string()),
Some(202),
Some("health-check".to_string()),
)
.await
.expect("reconfirmed connected record");
client
.report_managed_connection_settled(connection_id, true, Some(device_id))
.await
.expect("current-generation health confirmation");
let after = client.peer_session(device_id).await.expect("peer session");
assert!(after.settled_ready);
assert!(!after.replacement_pending);
assert_eq!(after.status, ConnectionState::Connected);
assert_eq!(after.readiness_state, ReadinessState::Routable);
assert_eq!(after.readiness_reason, "transport-healthy");
assert_eq!(after.active_transport_stable_id, Some(202));
assert_eq!(after.error.as_deref(), None);
}
#[tokio::test]
async fn reconcile_incoming_close_ignores_stale_closed_transport_after_generation_advance() {
let client = Client::new_with_app_tag(
crate::test_constants::TEST_PROJECT_ID.to_string(),
"test-app".to_string(),
Box::new(|| None),
);
client
.connection_manager
.upsert_pending(
"conn-stale-close".to_string(),
Some("node-stale-close".to_string()),
Some("device-stale-close".to_string()),
Some("node-stale-close".to_string()),
)
.await;
client
.connection_manager
.set_connected_with_transport(
"conn-stale-close",
Some("node-stale-close".to_string()),
Some(10),
Some("incoming".to_string()),
)
.await
.expect("connected record");
client
.connection_manager
.mark_transport_replaced(
"conn-stale-close",
Some(11),
Some("incoming-replacement".to_string()),
Some("replacement-in-progress".to_string()),
)
.await
.expect("replacement record");
let endpoint_id = "5d388fcaaa61efa536d78051e060928f6c1fff0983a32f6b532176b12328c3ac"
.parse()
.expect("endpoint id");
let resolution = client
.reconcile_incoming_transport_after_local_close(
"conn-stale-close",
endpoint_id,
"node-stale-close",
10,
"LocallyClosed",
)
.await;
assert_eq!(
resolution,
IncomingTransportCloseResolution::PreservedKeptTransport
);
let record = client
.connection_manager
.get_by_connection_id("conn-stale-close")
.await
.expect("record");
assert_eq!(record.state, ConnectionState::Connected);
assert_eq!(record.transport_stable_id, Some(11));
assert_eq!(record.transport_generation, 2);
assert_eq!(
record.status_reason.as_deref(),
Some("replacement-in-progress")
);
}
#[tokio::test]
async fn reconcile_incoming_close_ignores_stale_manual_disconnect_after_generation_advance() {
let client = Client::new_with_app_tag(
crate::test_constants::TEST_PROJECT_ID.to_string(),
"test-app".to_string(),
Box::new(|| None),
);
client
.connection_manager
.upsert_pending(
"conn-stale-manual-close".to_string(),
Some("node-stale-manual-close".to_string()),
Some("device-stale-manual-close".to_string()),
Some("node-stale-manual-close".to_string()),
)
.await;
client
.connection_manager
.set_connected_with_transport(
"conn-stale-manual-close",
Some("node-stale-manual-close".to_string()),
Some(10),
Some("incoming".to_string()),
)
.await
.expect("connected record");
client
.connection_manager
.mark_transport_replaced(
"conn-stale-manual-close",
Some(11),
Some("incoming-replacement".to_string()),
Some("replacement-in-progress".to_string()),
)
.await
.expect("replacement record");
let endpoint_id = "5d388fcaaa61efa536d78051e060928f6c1fff0983a32f6b532176b12328c3ac"
.parse()
.expect("endpoint id");
let resolution = client
.reconcile_incoming_transport_after_local_close(
"conn-stale-manual-close",
endpoint_id,
"node-stale-manual-close",
10,
crate::lifecycle_reason::REASON_MANUAL_DISCONNECT,
)
.await;
assert_eq!(
resolution,
IncomingTransportCloseResolution::PreservedKeptTransport
);
let record = client
.connection_manager
.get_by_connection_id("conn-stale-manual-close")
.await
.expect("record");
assert_eq!(record.state, ConnectionState::Connected);
assert_eq!(record.transport_stable_id, Some(11));
assert_eq!(record.transport_generation, 2);
assert_eq!(
record.status_reason.as_deref(),
Some("replacement-in-progress")
);
}
#[tokio::test]
async fn reconcile_incoming_close_reconfirms_readiness_for_kept_transport() {
let client_a = Client::new_with_app_tag(
crate::test_constants::TEST_PROJECT_ID.to_string(),
"test-a".to_string(),
Box::new(|| None),
);
let client_b = Client::new_with_app_tag(
crate::test_constants::TEST_PROJECT_ID.to_string(),
"test-b".to_string(),
Box::new(|| None),
);
let endpoint_a = Endpoint::builder(iroh::endpoint::presets::N0)
.alpns(vec![crate::native_node::PlutoniumProtocol::ALPN.to_vec()])
.relay_mode(RelayMode::Disabled)
.bind_addr(
"127.0.0.1:0"
.parse::<std::net::SocketAddr>()
.expect("socket addr"),
)
.expect("bind addr a")
.bind()
.await
.expect("bind endpoint a");
let endpoint_b = Endpoint::builder(iroh::endpoint::presets::N0)
.alpns(vec![crate::native_node::PlutoniumProtocol::ALPN.to_vec()])
.relay_mode(RelayMode::Disabled)
.bind_addr(
"127.0.0.1:0"
.parse::<std::net::SocketAddr>()
.expect("socket addr"),
)
.expect("bind addr b")
.bind()
.await
.expect("bind endpoint b");
client_a.adopt_endpoint(endpoint_a).await;
client_b.adopt_endpoint(endpoint_b).await;
let endpoint_a = client_a.get_endpoint().await.expect("endpoint a");
let endpoint_b = client_b.get_endpoint().await.expect("endpoint b");
let endpoint_a_id = endpoint_a.id();
let endpoint_b_id = endpoint_b.id();
let endpoint_b_addr = wait_for_endpoint_addr(&endpoint_b, 5_000)
.await
.expect("endpoint b addr");
client_a
.ensure_connected_addr(endpoint_b_id, endpoint_b_addr.clone())
.await
.expect("initial connection");
let kept_connection = tokio::time::timeout(Duration::from_secs(5), async {
loop {
if let Some(connection) = client_b.get_connection(endpoint_a_id).await {
break connection;
}
sleep(Duration::from_millis(50)).await;
}
})
.await
.expect("timed out waiting for kept connection");
let kept_stable_id = kept_connection.stable_id() as u64;
let remote_node_id = endpoint_a_id.to_string();
let local_node_id = endpoint_b_id.to_string();
let connection_id = Client::deterministic_connection_id(&local_node_id, &remote_node_id);
let synthetic_closed_stable_id = kept_stable_id.saturating_add(100);
client_b
.connection_manager
.upsert_pending(
connection_id.clone(),
Some(remote_node_id.clone()),
Some("device-a".to_string()),
Some(remote_node_id.clone()),
)
.await;
client_b
.connection_manager
.set_connected_with_transport(
&connection_id,
Some(remote_node_id.clone()),
Some(synthetic_closed_stable_id),
Some("incoming".to_string()),
)
.await
.expect("connected record");
client_b
.connection_manager
.set_device_id(&connection_id, "device-a".to_string())
.await
.expect("device binding");
let resolution = client_b
.reconcile_incoming_transport_after_local_close(
&connection_id,
endpoint_a_id,
&remote_node_id,
synthetic_closed_stable_id,
"LocallyClosed",
)
.await;
assert_eq!(
resolution,
IncomingTransportCloseResolution::PreservedKeptTransport
);
let settled = client_b
.wait_for_settled_peer("device-a", Some(2_000))
.await
.expect("settled peer snapshot");
assert!(
settled.settled_ready,
"expected recovered peer to be settled"
);
assert_eq!(
settled.active_connection_id.as_deref(),
Some(connection_id.as_str())
);
let record = client_b
.connection_manager
.get_by_connection_id(&connection_id)
.await
.expect("record");
assert_eq!(record.transport_stable_id, Some(kept_stable_id));
let managed_health = client_b
.managed_connection_health(&connection_id)
.await
.expect("managed health snapshot");
assert_eq!(
managed_health.status,
ManagedConnectionHealthStatus::Healthy
);
assert!(!managed_health.replacement_pending);
assert_eq!(
managed_health.active_transport_stable_id,
Some(kept_stable_id)
);
assert_eq!(managed_health.readiness_state, ReadinessState::Routable);
assert_eq!(
client_b
.managed_connection_bridge_action(&connection_id)
.await,
Some(ManagedConnectionBridgeAction::Healthy)
);
let health = client_b
.connection_manager
.peer_snapshot("device-a")
.await
.expect("peer snapshot");
assert_eq!(health.health, ConnectionHealth::Healthy);
}
#[tokio::test]
async fn identity_change_retires_conflicting_live_native_record() {
let client_a = Client::new_with_app_tag(
crate::test_constants::TEST_PROJECT_ID.to_string(),
"identity-old".to_string(),
Box::new(|| None),
);
let client_b = Client::new_with_app_tag(
crate::test_constants::TEST_PROJECT_ID.to_string(),
"identity-target".to_string(),
Box::new(|| None),
);
let endpoint_a = Endpoint::builder(iroh::endpoint::presets::N0)
.alpns(vec![crate::native_node::PlutoniumProtocol::ALPN.to_vec()])
.relay_mode(RelayMode::Disabled)
.bind_addr(
"127.0.0.1:0"
.parse::<std::net::SocketAddr>()
.expect("socket addr"),
)
.expect("bind addr a")
.bind()
.await
.expect("bind endpoint a");
let endpoint_b = Endpoint::builder(iroh::endpoint::presets::N0)
.alpns(vec![crate::native_node::PlutoniumProtocol::ALPN.to_vec()])
.relay_mode(RelayMode::Disabled)
.bind_addr(
"127.0.0.1:0"
.parse::<std::net::SocketAddr>()
.expect("socket addr"),
)
.expect("bind addr b")
.bind()
.await
.expect("bind endpoint b");
client_a.adopt_endpoint(endpoint_a).await;
client_b.adopt_endpoint(endpoint_b).await;
let endpoint_a = client_a.get_endpoint().await.expect("endpoint a");
let endpoint_b = client_b.get_endpoint().await.expect("endpoint b");
let endpoint_a_id = endpoint_a.id();
let endpoint_b_id = endpoint_b.id();
let endpoint_b_addr = wait_for_endpoint_addr(&endpoint_b, 5_000)
.await
.expect("endpoint b addr");
client_a
.ensure_connected_addr(endpoint_b_id, endpoint_b_addr)
.await
.expect("initial connection");
let kept_connection = tokio::time::timeout(Duration::from_secs(5), async {
loop {
if let Some(connection) = client_b.get_connection(endpoint_a_id).await {
break connection;
}
sleep(Duration::from_millis(50)).await;
}
})
.await
.expect("timed out waiting for target connection");
let old_remote_node_id = endpoint_a_id.to_string();
let local_node_id = endpoint_b_id.to_string();
let connection_id =
Client::deterministic_connection_id(&local_node_id, &old_remote_node_id);
client_b
.connection_manager
.upsert_pending(
connection_id.clone(),
Some(old_remote_node_id.clone()),
Some("device-a".to_string()),
Some(old_remote_node_id.clone()),
)
.await;
client_b
.connection_manager
.set_connected_with_transport(
&connection_id,
Some(old_remote_node_id.clone()),
Some(kept_connection.stable_id() as u64),
Some("incoming".to_string()),
)
.await
.expect("connected record");
client_b
.connection_manager
.set_device_id(&connection_id, "device-a".to_string())
.await
.expect("device binding");
client_b
.reconcile_authoritative_device_node("device-a", "node-new-identity")
.await;
tokio::time::timeout(Duration::from_secs(5), async {
loop {
if client_b
.connection_manager
.get_by_connection_id(&connection_id)
.await
.is_none()
{
break;
}
sleep(Duration::from_millis(50)).await;
}
})
.await
.expect("timed out waiting for stale record retirement");
tokio::time::timeout(Duration::from_secs(5), async {
loop {
if !client_b.is_connected(endpoint_a_id).await {
break;
}
sleep(Duration::from_millis(50)).await;
}
})
.await
.expect("timed out waiting for old live transport disconnect");
}
#[tokio::test]
async fn device_status_snapshots_mark_connected_healthy_peer_as_settled_ready() {
let client = Client::new_with_app_tag(
crate::test_constants::TEST_PROJECT_ID.to_string(),
"test-app".to_string(),
Box::new(|| None),
);
let remote_device = crate::signaling::Device {
app_tag: Some("test-app".to_string()),
device_id: "device-remote-1".to_string(),
user_id: Some("user-1".to_string()),
device_name: "Remote Device".to_string(),
platform_type: Some("web".to_string()),
capabilities: None,
session_id: None,
node_id: Some("node-remote-1".to_string()),
online: true,
ticket: Some("ticket-1".to_string()),
tag: Some("test-app".to_string()),
kind: Some("device".to_string()),
metadata: None,
last_seen_at: None,
expires_at: None,
created_at: None,
updated_at: None,
excluded_peers: vec![],
};
client
.connection_manager
.upsert_pending(
"conn-remote-1".to_string(),
Some("node-remote-1".to_string()),
Some(remote_device.device_id.clone()),
Some("node-remote-1".to_string()),
)
.await;
client
.connection_manager
.set_connected("conn-remote-1", Some("node-remote-1".to_string()))
.await;
client
.connection_manager
.set_health(
"conn-remote-1",
crate::connection_manager::ConnectionHealth::Healthy,
)
.await;
let snapshots = merge_device_status_snapshots(
vec![remote_device],
client.connection_manager.list_peer_snapshots().await,
);
let snapshot = snapshots.first().expect("snapshot");
assert_eq!(
snapshot.connection_status,
DeviceConnectionStatus::Connected
);
assert!(snapshot.settled_ready);
}
#[tokio::test]
async fn application_route_settlement_promotes_replacement_record_to_routable() {
let client = Client::new_with_app_tag(
crate::test_constants::TEST_PROJECT_ID.to_string(),
"test-app".to_string(),
Box::new(|| None),
);
let connection_id = "conn-route-settled";
let device_id = "device-route-settled";
let node_id = "node-route-settled";
client
.connection_manager
.upsert_pending(
connection_id.to_string(),
Some(node_id.to_string()),
Some(device_id.to_string()),
Some(node_id.to_string()),
)
.await;
client
.connection_manager
.set_connected_with_transport(
connection_id,
Some(node_id.to_string()),
Some(101),
Some("iroh".to_string()),
)
.await
.expect("connected record");
client
.connection_manager
.mark_transport_replaced(
connection_id,
Some(102),
Some("transient-disconnect".to_string()),
Some(crate::lifecycle_reason::REASON_REPLACEMENT_IN_PROGRESS.to_string()),
)
.await
.expect("replacement marker");
client
.connection_manager
.set_health(connection_id, ConnectionHealth::Stale)
.await;
let before = client
.managed_connection_health(connection_id)
.await
.expect("health before settlement");
assert!(!before.settled_ready);
assert!(matches!(
before.readiness_state,
ReadinessState::Settling | ReadinessState::AwaitingReplacement
));
let settled = client
.report_managed_connection_settled(connection_id, true, Some(device_id))
.await
.expect("settled snapshot");
assert!(settled.health == ConnectionHealth::Healthy);
let session = client
.peer_session(device_id)
.await
.expect("peer session after settlement");
assert!(session.settled_ready);
assert!(!session.replacement_pending);
assert_eq!(session.readiness_state, ReadinessState::Routable);
assert_eq!(session.readiness_reason, "transport-healthy");
let health = client
.managed_connection_health(connection_id)
.await
.expect("health after settlement");
assert!(health.settled_ready);
assert!(!health.replacement_pending);
assert_eq!(health.readiness_state, ReadinessState::Routable);
assert_eq!(health.readiness_reason, "transport-healthy");
let record = client
.connection_manager
.get_by_connection_id(connection_id)
.await
.expect("record after settlement");
assert_eq!(record.status_reason.as_deref(), None);
}
#[tokio::test]
async fn device_status_keeps_independent_webrtc_routable_when_base_health_is_stale() {
let client = Client::new_with_app_tag(
crate::test_constants::TEST_PROJECT_ID.to_string(),
"test-app".to_string(),
Box::new(|| None),
);
let remote_node_id = "5d388fcaaa61efa536d78051e060928f6c1fff0983a32f6b532176b12328c3ac";
let remote_device = crate::signaling::Device {
app_tag: Some("test-app".to_string()),
device_id: "device-independent-webrtc-status".to_string(),
user_id: Some("user-1".to_string()),
device_name: "Remote WebRTC Device".to_string(),
platform_type: Some("web".to_string()),
capabilities: None,
session_id: None,
node_id: Some(remote_node_id.to_string()),
online: true,
ticket: Some("ticket-webrtc".to_string()),
tag: Some("test-app".to_string()),
kind: Some("device".to_string()),
metadata: None,
last_seen_at: None,
expires_at: None,
created_at: None,
updated_at: None,
excluded_peers: vec![],
};
client
.connection_manager
.upsert_pending(
"conn-independent-webrtc-status".to_string(),
Some(remote_node_id.to_string()),
Some(remote_device.device_id.clone()),
Some(remote_node_id.to_string()),
)
.await;
client
.connection_manager
.set_connected_with_transport(
"conn-independent-webrtc-status",
Some(remote_node_id.to_string()),
None,
Some("transient-disconnect".to_string()),
)
.await
.expect("connected record");
client
.connection_manager
.report_transport_status(
"conn-independent-webrtc-status",
"webrtc".to_string(),
Some("iroh-relay".to_string()),
)
.await
.expect("transport status");
client
.connection_manager
.mark_transport_replaced(
"conn-independent-webrtc-status",
None,
Some("transient-disconnect".to_string()),
Some(crate::lifecycle_reason::REASON_REPLACEMENT_IN_PROGRESS.to_string()),
)
.await
.expect("replacement marker");
client
.connection_manager
.set_health(
"conn-independent-webrtc-status",
crate::connection_manager::ConnectionHealth::Stale,
)
.await;
let snapshots = merge_device_status_snapshots(
vec![remote_device.clone()],
client.connection_manager.list_peer_snapshots().await,
);
let snapshot = snapshots.first().expect("snapshot");
assert_eq!(
snapshot.connection_status,
DeviceConnectionStatus::Connecting
);
assert!(!snapshot.settled_ready);
assert_eq!(snapshot.readiness_state, ReadinessState::Settling);
assert_eq!(snapshot.active_transport, "webrtc");
client
.connection_manager
.set_health(
"conn-independent-webrtc-status",
crate::connection_manager::ConnectionHealth::Healthy,
)
.await;
let recovered = merge_device_status_snapshots(
vec![remote_device],
client.connection_manager.list_peer_snapshots().await,
);
let recovered = recovered.first().expect("recovered snapshot");
assert_eq!(
recovered.connection_status,
DeviceConnectionStatus::Connected
);
assert!(recovered.settled_ready);
assert_eq!(recovered.readiness_state, ReadinessState::Routable);
assert_eq!(recovered.active_transport, "webrtc");
}
#[tokio::test]
async fn device_status_snapshots_match_pending_native_peer_by_node_id_before_device_binding() {
let client = Client::new_with_app_tag(
crate::test_constants::TEST_PROJECT_ID.to_string(),
"test-app".to_string(),
Box::new(|| None),
);
let remote_device = crate::signaling::Device {
app_tag: Some("test-app".to_string()),
device_id: "device-remote-node-alias-1".to_string(),
user_id: Some("user-1".to_string()),
device_name: "Remote Device".to_string(),
platform_type: Some("desktop".to_string()),
capabilities: None,
session_id: None,
node_id: Some("node-remote-node-alias-1".to_string()),
online: true,
ticket: Some("ticket-node-alias-1".to_string()),
tag: Some("test-app".to_string()),
kind: Some("device".to_string()),
metadata: None,
last_seen_at: None,
expires_at: None,
created_at: None,
updated_at: None,
excluded_peers: vec![],
};
client
.connection_manager
.upsert_pending(
"conn-node-alias-1".to_string(),
Some("node-remote-node-alias-1".to_string()),
None,
Some("node-remote-node-alias-1".to_string()),
)
.await;
client
.connection_manager
.set_connected(
"conn-node-alias-1",
Some("node-remote-node-alias-1".to_string()),
)
.await;
client
.connection_manager
.set_health(
"conn-node-alias-1",
crate::connection_manager::ConnectionHealth::Healthy,
)
.await;
let snapshots = merge_device_status_snapshots(
vec![remote_device],
client.connection_manager.list_peer_snapshots().await,
);
let snapshot = snapshots.first().expect("snapshot");
assert_eq!(
snapshot.connection_status,
DeviceConnectionStatus::Connected
);
assert!(snapshot.settled_ready);
assert_eq!(snapshot.connection_id.as_deref(), Some("conn-node-alias-1"));
}
#[tokio::test]
async fn peer_session_snapshot_reports_active_and_candidate_connections() {
let client = Client::new_with_app_tag(
crate::test_constants::TEST_PROJECT_ID.to_string(),
"test-app".to_string(),
Box::new(|| None),
);
client
.connection_manager
.upsert_pending(
"conn-1".to_string(),
Some("node-1".to_string()),
Some("device-1".to_string()),
Some("node-1".to_string()),
)
.await;
client
.connection_manager
.set_connected("conn-1", Some("node-1".to_string()))
.await;
client
.connection_manager
.set_health(
"conn-1",
crate::connection_manager::ConnectionHealth::Healthy,
)
.await;
client
.connection_manager
.upsert_pending(
"conn-2".to_string(),
Some("node-2".to_string()),
Some("device-1".to_string()),
Some("node-2".to_string()),
)
.await;
let snapshot = client
.peer_session("device-1")
.await
.expect("peer session snapshot");
assert_eq!(snapshot.peer_id, "device-1");
assert_eq!(snapshot.active_connection_id.as_deref(), Some("conn-1"));
assert_eq!(
snapshot.candidate_connection_ids,
vec!["conn-2".to_string()]
);
assert!(snapshot.settled_ready);
}
#[tokio::test]
async fn peer_session_snapshot_rehydrates_current_accepted_share_scope() {
let client = Client::new_with_app_tag(
crate::test_constants::TEST_PROJECT_ID.to_string(),
"test-app".to_string(),
Box::new(|| None),
);
let connection_id = "conn-share-admission-before-record";
let scope = "share:session-native-host";
client.register_session_token("share-token".into(), scope.into(), 0);
client
.validate_session_token_for_connection("share-token", connection_id)
.await
.expect("accept token before the connection record exists");
assert!(client
.connection_manager
.get_scopes(connection_id)
.await
.is_empty());
client
.connection_manager
.upsert_pending(
connection_id.to_string(),
Some("node-share-native-host".to_string()),
None,
Some("node-share-native-host".to_string()),
)
.await;
client
.connection_manager
.set_connected(connection_id, Some("node-share-native-host".to_string()))
.await;
let snapshot = client
.peer_session(connection_id)
.await
.expect("peer session snapshot");
assert_eq!(
snapshot.active_connection_id.as_deref(),
Some(connection_id)
);
assert_eq!(snapshot.scopes, vec![scope.to_string()]);
}
#[tokio::test]
async fn peer_session_lookup_chooses_best_snapshot_across_aliases() {
let client = Client::new_with_app_tag(
crate::test_constants::TEST_PROJECT_ID.to_string(),
"test-app".to_string(),
Box::new(|| None),
);
client
.connection_manager
.upsert_pending(
"conn-cross-alias-stale".to_string(),
Some("node-cross-alias-runtime".to_string()),
Some("device-cross-alias-runtime".to_string()),
Some("node-cross-alias-runtime".to_string()),
)
.await;
client
.connection_manager
.set_connected_with_transport(
"conn-cross-alias-stale",
Some("node-cross-alias-runtime".to_string()),
None,
Some("transient-disconnect".to_string()),
)
.await
.expect("stale connected record");
client
.connection_manager
.upsert_pending(
"conn-cross-alias-live".to_string(),
Some("node-cross-alias-runtime".to_string()),
None,
Some("node-cross-alias-runtime".to_string()),
)
.await;
client
.connection_manager
.set_connected_with_transport(
"conn-cross-alias-live",
Some("node-cross-alias-runtime".to_string()),
Some(42),
Some("webrtc".to_string()),
)
.await
.expect("live connected record");
client
.connection_manager
.report_transport_status(
"conn-cross-alias-live",
"webrtc".to_string(),
Some("iroh-relay".to_string()),
)
.await
.expect("live transport status");
client
.connection_manager
.set_health("conn-cross-alias-live", ConnectionHealth::Healthy)
.await
.expect("live health");
let session = client
.peer_session("device-cross-alias-runtime")
.await
.expect("peer session");
assert_eq!(session.peer_id, "conn-cross-alias-live");
assert_eq!(
session.active_connection_id.as_deref(),
Some("conn-cross-alias-live")
);
assert!(session.settled_ready);
assert_eq!(session.readiness_state, ReadinessState::Routable);
assert_eq!(session.active_transport, "webrtc");
}
#[tokio::test]
async fn wait_for_settled_peer_returns_settled_snapshot() {
let client = std::sync::Arc::new(Client::new_with_app_tag(
crate::test_constants::TEST_PROJECT_ID.to_string(),
"test-app".to_string(),
Box::new(|| None),
));
let client_clone = client.clone();
tokio::spawn(async move {
tokio::time::sleep(std::time::Duration::from_millis(50)).await;
client_clone
.connection_manager
.upsert_pending(
"conn-wait-1".to_string(),
Some("node-wait-1".to_string()),
Some("device-wait-1".to_string()),
Some("node-wait-1".to_string()),
)
.await;
client_clone
.connection_manager
.set_connected("conn-wait-1", Some("node-wait-1".to_string()))
.await;
client_clone
.connection_manager
.set_health(
"conn-wait-1",
crate::connection_manager::ConnectionHealth::Healthy,
)
.await;
});
let snapshot = client
.wait_for_settled_peer("device-wait-1", Some(1_000))
.await
.expect("settled snapshot");
assert_eq!(snapshot.peer_id, "device-wait-1");
assert!(snapshot.settled_ready);
}
#[tokio::test]
async fn peer_session_reads_do_not_adopt_an_unowned_live_transport() {
let client_a = Client::new_with_app_tag(
crate::test_constants::TEST_PROJECT_ID.to_string(),
"test-app".to_string(),
Box::new(|| None),
);
let client_b = Client::new_with_app_tag(
crate::test_constants::TEST_PROJECT_ID.to_string(),
"test-app".to_string(),
Box::new(|| None),
);
let endpoint_a = Endpoint::builder(iroh::endpoint::presets::N0)
.alpns(vec![crate::native_node::PlutoniumProtocol::ALPN.to_vec()])
.relay_mode(RelayMode::Disabled)
.bind_addr(
"127.0.0.1:0"
.parse::<std::net::SocketAddr>()
.expect("socket addr"),
)
.expect("bind addr a")
.bind()
.await
.expect("bind endpoint a");
let endpoint_b = Endpoint::builder(iroh::endpoint::presets::N0)
.alpns(vec![crate::native_node::PlutoniumProtocol::ALPN.to_vec()])
.relay_mode(RelayMode::Disabled)
.bind_addr(
"127.0.0.1:0"
.parse::<std::net::SocketAddr>()
.expect("socket addr"),
)
.expect("bind addr b")
.bind()
.await
.expect("bind endpoint b");
client_a.adopt_endpoint(endpoint_a).await;
client_b.adopt_endpoint(endpoint_b).await;
let endpoint_a = client_a.get_endpoint().await.expect("endpoint a");
let endpoint_b = client_b.get_endpoint().await.expect("endpoint b");
let endpoint_a_id = endpoint_a.id();
let endpoint_b_id = endpoint_b.id();
let endpoint_b_addr = wait_for_endpoint_addr(&endpoint_b, 5_000)
.await
.expect("endpoint b addr");
client_a
.ensure_connected_addr(endpoint_b_id, endpoint_b_addr)
.await
.expect("initial connection");
tokio::time::timeout(Duration::from_secs(5), async {
loop {
if client_b.get_connection(endpoint_a_id).await.is_some() {
break;
}
sleep(Duration::from_millis(50)).await;
}
})
.await
.expect("timed out waiting for live transport");
for record in client_b.connection_manager.list_all().await {
client_b
.connection_manager
.remove(&record.connection_id)
.await;
}
assert!(client_b.connection_manager.list_all().await.is_empty());
let endpoint_lookup = client_b.peer_session(&endpoint_a_id.to_string()).await;
assert!(endpoint_lookup.is_none());
assert!(client_b.peer_sessions().await.is_empty());
assert!(client_b
.wait_for_settled_peer(&endpoint_a_id.to_string(), Some(10))
.await
.is_none());
assert!(client_b.connection_manager.list_all().await.is_empty());
}
#[tokio::test]
async fn open_peer_bi_rejects_unsettled_peer_before_transport_lookup() {
let client = Client::new_with_app_tag(
crate::test_constants::TEST_PROJECT_ID.to_string(),
"test-app".to_string(),
Box::new(|| None),
);
client
.connection_manager
.upsert_pending(
"conn-open-1".to_string(),
Some("node-open-1".to_string()),
Some("device-open-1".to_string()),
Some("node-open-1".to_string()),
)
.await;
client
.connection_manager
.set_connected("conn-open-1", Some("node-open-1".to_string()))
.await;
let error = client
.open_peer_bi("device-open-1", Some(10))
.await
.expect_err("unsettled peer should fail before transport open");
assert!(
error.to_string().contains("not settled"),
"unexpected error: {}",
error
);
}
#[tokio::test]
async fn open_peer_bi_transport_only_does_not_require_settled_readiness() {
let client = Client::new_with_app_tag(
crate::test_constants::TEST_PROJECT_ID.to_string(),
"test-app".to_string(),
Box::new(|| None),
);
client
.connection_manager
.upsert_pending(
"conn-transport-only-1".to_string(),
Some("node-transport-only-1".to_string()),
Some("device-transport-only-1".to_string()),
Some("node-transport-only-1".to_string()),
)
.await;
client
.connection_manager
.set_connected(
"conn-transport-only-1",
Some("node-transport-only-1".to_string()),
)
.await;
let error = client
.open_peer_bi_transport_only("device-transport-only-1", Some(10))
.await
.expect_err("missing live transport should still fail");
assert!(
!error.to_string().contains("not settled"),
"transport-only open must not reuse strict settled readiness errors: {}",
error
);
}
#[tokio::test]
async fn open_peer_bi_transport_only_requires_managed_admission() {
let client = Client::new_with_app_tag(
crate::test_constants::TEST_PROJECT_ID.to_string(),
"test-app".to_string(),
Box::new(|| None),
);
client
.session_token_registry
.register("managed-token".to_string(), "user-device", 0);
client
.connection_manager
.upsert_pending(
"conn-transport-only-managed".to_string(),
Some("node-transport-only-managed".to_string()),
Some("device-transport-only-managed".to_string()),
Some("node-transport-only-managed".to_string()),
)
.await;
client
.connection_manager
.set_connected(
"conn-transport-only-managed",
Some("node-transport-only-managed".to_string()),
)
.await;
let error = client
.open_peer_bi_transport_only("device-transport-only-managed", Some(10))
.await
.expect_err("pending managed admission must fail before transport open");
assert!(
error.to_string().contains("session-admission-pending"),
"unexpected error: {}",
error
);
}
#[tokio::test]
async fn open_peer_bi_rejects_stale_settled_projection_without_live_transport() {
let client = Client::new_with_app_tag(
crate::test_constants::TEST_PROJECT_ID.to_string(),
"test-app".to_string(),
Box::new(|| None),
);
let remote_node_id = "844465046e89fce03a8f96df9e289ab1a4336f05d1d4ef67f2d4598dd7ad0a2a";
client
.connection_manager
.upsert_pending(
"conn-open-2".to_string(),
Some(remote_node_id.to_string()),
Some("device-open-2".to_string()),
Some(remote_node_id.to_string()),
)
.await;
client
.connection_manager
.set_connected_with_transport(
"conn-open-2",
Some(remote_node_id.to_string()),
Some(88),
Some("test-health-proof".to_string()),
)
.await
.expect("connected transport");
client
.connection_manager
.set_health(
"conn-open-2",
crate::connection_manager::ConnectionHealth::Healthy,
)
.await;
let error = client
.open_peer_bi("device-open-2", Some(10))
.await
.expect_err("stale settled projection must not authorize protocol traffic");
assert!(
error.to_string().contains("no active transport"),
"unexpected error: {}",
error
);
}
#[tokio::test]
async fn peer_snapshot_read_preserves_settled_webrtc_peer_when_iroh_is_gone() {
let client = Client::new_with_app_tag(
crate::test_constants::TEST_PROJECT_ID.to_string(),
"test-app".to_string(),
Box::new(|| None),
);
let remote_node_id = "844465046e89fce03a8f96df9e289ab1a4336f05d1d4ef67f2d4598dd7ad0a2a";
client
.connection_manager
.upsert_pending(
"conn-open-webrtc".to_string(),
Some(remote_node_id.to_string()),
Some("device-open-webrtc".to_string()),
Some(remote_node_id.to_string()),
)
.await;
client
.connection_manager
.set_connected_with_transport(
"conn-open-webrtc",
Some(remote_node_id.to_string()),
Some(77),
Some("incoming".to_string()),
)
.await
.expect("connected record");
client
.connection_manager
.set_health(
"conn-open-webrtc",
crate::connection_manager::ConnectionHealth::Healthy,
)
.await;
client
.connection_manager
.report_transport_status(
"conn-open-webrtc",
"webrtc".to_string(),
Some("iroh-relay".to_string()),
)
.await
.expect("peer snapshot");
let snapshot = client
.peer_snapshot("conn-open-webrtc")
.await
.expect("snapshot should remain");
assert_eq!(snapshot.active_transport, "webrtc");
assert_eq!(snapshot.parallel_transport.as_deref(), Some("iroh-relay"));
assert!(
client
.connection_manager
.get_by_connection_id("conn-open-webrtc")
.await
.is_some(),
"settled upgraded peer should not be retired just because iroh rotated",
);
}
#[tokio::test]
async fn peer_session_read_never_retires_base_record_needed_by_recovery_owner() {
let client = Client::new_with_app_tag(
crate::test_constants::TEST_PROJECT_ID.to_string(),
"test-app".to_string(),
Box::new(|| None),
);
let connection_id = "conn-stale-base-read";
let device_id = "device-stale-base-read";
let remote_node_id = "844465046e89fce03a8f96df9e289ab1a4336f05d1d4ef67f2d4598dd7ad0a2a";
client
.connection_manager
.upsert_pending(
connection_id.to_string(),
Some(remote_node_id.to_string()),
Some(device_id.to_string()),
Some(remote_node_id.to_string()),
)
.await;
client
.connection_manager
.set_connected_with_transport(
connection_id,
Some(remote_node_id.to_string()),
Some(77),
Some("iroh-relay".to_string()),
)
.await
.expect("connected record");
client
.connection_manager
.set_health(
connection_id,
crate::connection_manager::ConnectionHealth::Healthy,
)
.await;
for _ in 0..3 {
let session = client
.peer_session(device_id)
.await
.expect("stale transport remains addressable while recovery owns it");
assert_eq!(session.active_connection_id.as_deref(), Some(connection_id));
}
assert!(
client
.connection_manager
.get_by_connection_id(connection_id)
.await
.is_some(),
"read-side observation must not retire the lifecycle owner's record",
);
}
#[tokio::test]
async fn inspect_incoming_native_main_frame_parses_native_handshake() {
let client = Client::new_with_app_tag(
crate::test_constants::TEST_PROJECT_ID.to_string(),
"test-app".to_string(),
Box::new(|| None),
);
let frame = serde_json::json!({
"channel": "handshake",
"action": "request",
"payload": {
"deviceId": "c7d6b730-ff20-4cc3-bfdf-c2606f2a70ef",
},
"timestamp": 123,
})
.to_string()
.into_bytes();
let inspected = client
.inspect_incoming_native_main_frame("conn-native-1", Some("node-1"), None, &frame)
.await
.expect("native handshake should parse");
match inspected {
crate::native_protocol::InspectedMainFrame::NativeMessage { message, handshake } => {
assert_eq!(message.channel, "handshake");
assert_eq!(
handshake
.as_ref()
.and_then(|value| value.claimed_device_id.as_deref()),
Some("c7d6b730-ff20-4cc3-bfdf-c2606f2a70ef")
);
assert_eq!(handshake.and_then(|value| value.admitted_device_id), None);
}
other => panic!("expected native message, got {:?}", other),
}
}
#[tokio::test]
async fn inspect_incoming_native_main_frame_emits_peer_data_event() {
let client = Client::new_with_app_tag(
crate::test_constants::TEST_PROJECT_ID.to_string(),
"test-app".to_string(),
Box::new(|| None),
);
client
.connection_manager
.upsert_pending(
"conn-peer-data-1".to_string(),
Some("remote-node-1".to_string()),
None,
Some("remote-node-1".to_string()),
)
.await;
client
.connection_manager
.set_connected_with_transport(
"conn-peer-data-1",
None,
Some(10),
Some("iroh-relay".to_string()),
)
.await;
let mut rx = client.subscribe_native_peer_data();
let key = [11u8; crate::application_crypto::APPLICATION_KEY_BYTES];
let nonce = [12u8; crate::application_crypto::APPLICATION_NONCE_BYTES];
client.set_connection_application_crypto_key("conn-peer-data-1", key);
let protected = crate::application_crypto::protect_application_payload_with_nonce(
&key,
0,
b"hello over iroh peer data",
&nonce,
)
.expect("payload should encrypt");
let frame = serde_json::to_vec(&crate::native_protocol::NativeMainMessage::peer_data(
&protected,
))
.expect("peer data message should serialize");
let inspected = client
.inspect_incoming_native_main_frame(
"conn-peer-data-1",
Some("remote-node-1"),
None,
&frame,
)
.await
.expect("peer data frame should inspect");
assert!(matches!(
inspected,
crate::native_protocol::InspectedMainFrame::ForwardOpaque
));
let event = tokio::time::timeout(Duration::from_secs(1), rx.recv())
.await
.expect("native peer data event should arrive")
.expect("native peer data channel should stay open");
assert_eq!(event.connection_id, "conn-peer-data-1");
assert_eq!(event.remote_node_id.as_deref(), Some("remote-node-1"));
assert_eq!(event.transport, crate::transport_label::IROH);
assert_eq!(event.transport_stable_id, 10);
assert_eq!(event.transport_generation, 1);
assert_eq!(event.route_generation, 0);
assert_eq!(event.payload, b"hello over iroh peer data");
}
#[tokio::test]
async fn inbound_browser_application_frame_emits_peer_data_event() {
let client = Client::new_with_app_tag(
crate::test_constants::TEST_PROJECT_ID.to_string(),
"test-app".to_string(),
Box::new(|| None),
);
client
.connection_manager
.upsert_pending(
"conn-browser-app-1".to_string(),
Some("browser-node-1".to_string()),
None,
Some("browser-node-1".to_string()),
)
.await;
client
.connection_manager
.set_connected_with_transport(
"conn-browser-app-1",
None,
Some(20),
Some("iroh-relay".to_string()),
)
.await;
let mut rx = client.subscribe_native_peer_data();
let key = [13u8; crate::application_crypto::APPLICATION_KEY_BYTES];
let nonce = [14u8; crate::application_crypto::APPLICATION_NONCE_BYTES];
client.set_connection_application_crypto_key("conn-browser-app-1", key);
let protected = crate::application_crypto::protect_application_payload_with_nonce(
&key,
0,
b"hello from browser app payload",
&nonce,
)
.expect("payload should encrypt");
let mut frame = Vec::with_capacity(1 + protected.len());
frame.push(0);
frame.extend_from_slice(&protected);
let handled = client
.handle_inbound_peer_application_frame_for_transport(
"conn-browser-app-1",
Some("browser-node-1"),
crate::transport_label::IROH,
Some(20),
&frame,
)
.await
.expect("browser app frame should open");
assert!(handled);
let event = tokio::time::timeout(Duration::from_secs(1), rx.recv())
.await
.expect("native peer data event should arrive")
.expect("native peer data channel should stay open");
assert_eq!(event.connection_id, "conn-browser-app-1");
assert_eq!(event.remote_node_id.as_deref(), Some("browser-node-1"));
assert_eq!(event.transport, crate::transport_label::IROH);
assert_eq!(event.transport_stable_id, 20);
assert_eq!(event.transport_generation, 1);
assert_eq!(event.route_generation, 0);
assert_eq!(event.payload, b"hello from browser app payload");
}
#[test]
#[allow(deprecated)]
fn legacy_inbound_application_frame_api_fails_closed() {
let client = Client::new_with_app_tag(
crate::test_constants::TEST_PROJECT_ID.to_string(),
"test-app".to_string(),
Box::new(|| None),
);
let error = client
.handle_inbound_peer_application_frame(
"conn-legacy-frame",
Some("remote-node"),
crate::transport_label::IROH,
&[0, 1, 2, 3],
)
.expect_err("legacy API must not emit data without route provenance");
assert!(error.to_string().contains("route provenance is required"));
}
#[tokio::test]
async fn queued_peer_data_retains_retired_generation_identity_after_replacement() {
let client = Client::new_with_app_tag(
crate::test_constants::TEST_PROJECT_ID.to_string(),
"test-app".to_string(),
Box::new(|| None),
);
let connection_id = "conn-queued-generation";
client
.connection_manager
.upsert_pending(
connection_id.to_string(),
Some("remote-node".to_string()),
None,
Some("remote-node".to_string()),
)
.await;
client
.connection_manager
.set_connected_with_transport(
connection_id,
None,
Some(101),
Some("iroh-relay".to_string()),
)
.await;
let mut rx = client.subscribe_native_peer_data();
let frame = serde_json::to_vec(&crate::native_protocol::NativeMainMessage::peer_data(
b"queued-before-replacement",
))
.expect("peer data message should serialize");
client
.inspect_incoming_native_main_frame_for_transport(
connection_id,
Some("remote-node"),
None,
Some(101),
&frame,
)
.await
.expect("current generation should enqueue peer data");
let replacement = client
.connection_manager
.set_connected_with_transport(
connection_id,
None,
Some(202),
Some("iroh-quic".to_string()),
)
.await
.expect("replacement record");
let queued = rx.recv().await.expect("queued old-generation event");
assert_eq!(queued.transport_stable_id, 101);
assert_eq!(queued.transport_generation, 1);
assert_eq!(queued.route_generation, 0);
assert_eq!(replacement.transport_stable_id, Some(202));
assert_eq!(replacement.transport_generation, 2);
assert_ne!(
queued.transport_generation,
replacement.transport_generation
);
}
#[tokio::test]
async fn inspect_incoming_native_main_frame_rejects_missing_ts_token_when_registry_active() {
let client = Client::new_with_app_tag(
crate::test_constants::TEST_PROJECT_ID.to_string(),
"test-app".to_string(),
Box::new(|| None),
);
client.register_session_token("token-1".into(), "share".into(), 0);
let json = serde_json::json!({
"type": "handshake",
"action": "hello",
});
let mut frame = vec![0x00];
frame.extend_from_slice(json.to_string().as_bytes());
let error = client
.inspect_incoming_native_main_frame("conn-ts-1", Some("node-1"), None, &frame)
.await
.expect_err("missing token should be rejected");
assert_eq!(error, "session-token-required");
}
#[tokio::test]
async fn managed_connection_adoption_returns_identity_aware_snapshot() {
let client = Client::new_with_app_tag(
crate::test_constants::TEST_PROJECT_ID.to_string(),
"test-app".to_string(),
Box::new(|| None),
);
client
.connection_manager
.upsert_pending(
"conn-adopt-1".to_string(),
Some("node-adopt-1".to_string()),
Some("device-adopt-1".to_string()),
Some("node-adopt-1".to_string()),
)
.await;
client
.connection_manager
.set_connected_with_transport(
"conn-adopt-1",
Some("node-adopt-1".to_string()),
Some(22),
Some("managed".to_string()),
)
.await
.expect("managed record");
client
.connection_manager
.set_device_id("conn-adopt-1", "device-adopt-1".to_string())
.await
.expect("managed device binding");
let record = client
.connection_manager
.get_by_connection_id("conn-adopt-1")
.await
.expect("record exists");
let adoption = client
.managed_connection_adoption(&record)
.await
.expect("adoption snapshot");
assert_eq!(adoption.connection_id, "conn-adopt-1");
assert_eq!(adoption.node_id, "node-adopt-1");
assert_eq!(adoption.device_id.as_deref(), Some("device-adopt-1"));
assert_eq!(adoption.transport_generation, 1);
assert!(
!adoption.main_stream_ready,
"identity alone must not make an unadmitted transport routable"
);
}
#[tokio::test]
async fn inspect_ts_handshake_with_valid_token_succeeds() {
let client = Client::new_with_app_tag(
crate::test_constants::TEST_PROJECT_ID.to_string(),
"test-app".to_string(),
Box::new(|| None),
);
client.register_session_token("token-valid".into(), "share".into(), 1);
let json = serde_json::json!({
"type": "handshake",
"action": "hello",
"sessionToken": "token-valid",
});
let mut frame = vec![0x00];
frame.extend_from_slice(json.to_string().as_bytes());
let result = client
.inspect_incoming_native_main_frame("conn-ts-valid", Some("node-1"), None, &frame)
.await
.expect("valid token should be accepted");
assert!(matches!(
result,
crate::native_protocol::InspectedMainFrame::ForwardOpaque
));
}
#[tokio::test]
async fn inspect_ts_capability_update_after_admission_does_not_require_token() {
let client = Client::new_with_app_tag(
crate::test_constants::TEST_PROJECT_ID.to_string(),
"test-app".to_string(),
Box::new(|| None),
);
client.register_session_token("token-valid".into(), "share".into(), 1);
let hello = serde_json::json!({
"type": "handshake",
"action": "hello",
"sessionToken": "token-valid",
});
let mut hello_frame = vec![0x00];
hello_frame.extend_from_slice(hello.to_string().as_bytes());
client
.inspect_incoming_native_main_frame(
"conn-ts-update",
Some("node-1"),
None,
&hello_frame,
)
.await
.expect("hello should admit the connection");
let update = serde_json::json!({
"type": "handshake",
"action": "capability-update",
"capabilities": {
"webrtc": true,
},
});
let mut update_frame = vec![0x00];
update_frame.extend_from_slice(update.to_string().as_bytes());
let result = client
.inspect_incoming_native_main_frame(
"conn-ts-update",
Some("node-1"),
None,
&update_frame,
)
.await
.expect("capability-update should reuse existing admission");
assert!(matches!(
result,
crate::native_protocol::InspectedMainFrame::ForwardOpaque
));
}
#[tokio::test]
async fn inspect_opaque_frame_rejected_when_registry_active_and_not_admitted() {
let client = Client::new_with_app_tag(
crate::test_constants::TEST_PROJECT_ID.to_string(),
"test-app".to_string(),
Box::new(|| None),
);
client.register_session_token("token-1".into(), "share".into(), 0);
let error = client
.inspect_incoming_native_main_frame(
"conn-opaque-1",
Some("node-1"),
None,
b"\x01\x02\x03",
)
.await
.expect_err("opaque frame should be rejected when not admitted");
assert_eq!(error, "session-token-required");
}
#[tokio::test]
async fn inspect_opaque_frame_allowed_when_registry_inactive() {
let client = Client::new_with_app_tag(
crate::test_constants::TEST_PROJECT_ID.to_string(),
"test-app".to_string(),
Box::new(|| None),
);
let result = client
.inspect_incoming_native_main_frame(
"conn-opaque-2",
Some("node-1"),
None,
b"\x01\x02\x03",
)
.await
.expect("opaque frame should pass when no registry");
assert!(matches!(
result,
crate::native_protocol::InspectedMainFrame::ForwardOpaque
));
}
#[tokio::test]
async fn non_handshake_before_handshake_does_not_permanently_reject() {
let client = Client::new_with_app_tag(
crate::test_constants::TEST_PROJECT_ID.to_string(),
"test-app".to_string(),
Box::new(|| None),
);
client.register_session_token("token-1".into(), "share".into(), 1);
let event_frame = serde_json::json!({
"channel": "status",
"action": "event",
"payload": {},
"timestamp": 100,
})
.to_string()
.into_bytes();
let err = client
.inspect_incoming_native_main_frame("conn-race-1", Some("node-1"), None, &event_frame)
.await
.expect_err("non-handshake should be rejected when pending");
assert_eq!(err, "session-token-required");
let _handshake_frame = serde_json::json!({
"channel": "handshake",
"action": "request",
"payload": { "deviceId": "device-race-1" },
"timestamp": 101,
})
.to_string()
.into_bytes();
let admission = client.session_admission("conn-race-1");
assert!(
matches!(admission, crate::session_token::SessionAdmission::Pending),
"admission should still be Pending after transient rejection, got {:?}",
admission
);
}
#[tokio::test]
async fn non_handshake_binding_not_attached_to_non_handshake_messages() {
let client = Client::new_with_app_tag(
crate::test_constants::TEST_PROJECT_ID.to_string(),
"test-app".to_string(),
Box::new(|| None),
);
let event_frame = serde_json::json!({
"channel": "status",
"action": "event",
"payload": {},
"timestamp": 100,
})
.to_string()
.into_bytes();
let result = client
.inspect_incoming_native_main_frame(
"conn-no-binding",
Some("node-1"),
Some("known-device-1"),
&event_frame,
)
.await
.expect("should parse");
match result {
crate::native_protocol::InspectedMainFrame::NativeMessage { handshake, .. } => {
assert!(
handshake.is_none(),
"non-handshake messages should not carry a NativeHandshakeBinding"
);
}
other => panic!("expected NativeMessage, got {:?}", other),
}
}
#[tokio::test]
async fn ensure_native_stream_admitted_does_not_persist_rejection() {
let client = Client::new_with_app_tag(
crate::test_constants::TEST_PROJECT_ID.to_string(),
"test-app".to_string(),
Box::new(|| None),
);
client.register_session_token("token-1".into(), "share".into(), 0);
let err = client
.ensure_native_stream_admitted("conn-stream-1", Some("node-1"), None)
.expect_err("should reject unadmitted stream");
assert_eq!(err, "session-token-required");
let admission = client.session_admission("conn-stream-1");
assert!(
matches!(admission, crate::session_token::SessionAdmission::Pending),
"stream rejection should not persist, got {:?}",
admission
);
}
#[tokio::test]
async fn inspect_session_token_presentation_validates_and_admits() {
let client = Client::new_with_app_tag(
crate::test_constants::TEST_PROJECT_ID.to_string(),
"test-app".to_string(),
Box::new(|| None),
);
client.register_session_token("share-token-1".into(), "share".into(), 1);
let msg =
crate::native_protocol::NativeMainMessage::session_token_presentation("share-token-1");
let frame = serde_json::to_vec(&msg).unwrap();
let result = client
.inspect_incoming_native_main_frame("conn-token-1", Some("node-1"), None, &frame)
.await
.expect("valid token presentation should succeed");
match result {
crate::native_protocol::InspectedMainFrame::SessionTokenResponse {
response,
accepted,
is_first_presentation,
} => {
assert!(accepted);
assert!(is_first_presentation);
assert_eq!(response.session_token_approved(), Some(true));
assert_eq!(response.approved_session_scope().as_deref(), Some("share"));
}
other => panic!("expected session-token response, got {other:?}"),
}
let admission = client.session_admission("conn-token-1");
assert!(
matches!(
admission,
crate::session_token::SessionAdmission::Accepted { .. }
),
"connection should be admitted after token presentation, got {:?}",
admission
);
}
#[tokio::test]
async fn inspect_session_token_presentation_rejects_invalid_token() {
let client = Client::new_with_app_tag(
crate::test_constants::TEST_PROJECT_ID.to_string(),
"test-app".to_string(),
Box::new(|| None),
);
client.register_session_token("real-token".into(), "share".into(), 1);
let msg =
crate::native_protocol::NativeMainMessage::session_token_presentation("wrong-token");
let frame = serde_json::to_vec(&msg).unwrap();
let result = client
.inspect_incoming_native_main_frame("conn-bad-token", Some("node-1"), None, &frame)
.await
.expect("invalid token should produce a wire rejection");
match result {
crate::native_protocol::InspectedMainFrame::SessionTokenResponse {
response,
accepted,
is_first_presentation,
} => {
assert!(!accepted);
assert!(!is_first_presentation);
assert_eq!(response.session_token_approved(), Some(false));
assert!(response
.session_token_error()
.is_some_and(|reason| reason.contains("token")));
}
other => panic!("expected session-token rejection, got {other:?}"),
}
}
#[tokio::test]
async fn inspect_session_token_presentation_ignored_when_registry_inactive() {
let client = Client::new_with_app_tag(
crate::test_constants::TEST_PROJECT_ID.to_string(),
"test-app".to_string(),
Box::new(|| None),
);
let msg =
crate::native_protocol::NativeMainMessage::session_token_presentation("some-token");
let frame = serde_json::to_vec(&msg).unwrap();
let result = client
.inspect_incoming_native_main_frame("conn-no-reg", Some("node-1"), None, &frame)
.await
.expect("should pass when registry inactive");
assert!(matches!(
result,
crate::native_protocol::InspectedMainFrame::NativeMessage { .. }
));
}
#[tokio::test]
async fn native_user_device_compound_ticket_connection_allows_token_then_handshake() {
let host = Client::new_with_app_tag(
crate::test_constants::TEST_PROJECT_ID.to_string(),
"test-app".to_string(),
Box::new(|| None),
);
host.register_session_token("user-device-token".into(), "user-device".into(), 0);
let connection_id = "conn-user-device-sequence";
let peer_node_id = "peer-node-1";
let token_message = crate::native_protocol::NativeMainMessage::session_token_presentation(
"user-device-token",
);
let token_frame = serde_json::to_vec(&token_message).expect("serialize token message");
let token_result = host
.inspect_incoming_native_main_frame(
connection_id,
Some(peer_node_id),
None,
&token_frame,
)
.await
.expect("token presentation should be accepted");
assert!(matches!(
token_result,
InspectedMainFrame::SessionTokenResponse {
accepted: true,
is_first_presentation: true,
..
}
));
assert!(matches!(
host.session_admission(connection_id),
crate::session_token::SessionAdmission::Accepted { .. }
));
let handshake = NativeMainMessage {
channel: "handshake".to_string(),
action: NativeMessageAction::Request,
request_id: None,
payload: serde_json::json!({ "deviceId": "device-native-token-test" }),
timestamp: 123,
from: None,
};
let handshake_frame = serde_json::to_vec(&handshake).expect("serialize handshake");
let handshake_result = host
.inspect_incoming_native_main_frame(
connection_id,
Some(peer_node_id),
None,
&handshake_frame,
)
.await
.expect("handshake after token should be accepted");
match handshake_result {
InspectedMainFrame::NativeMessage { message, .. } => {
assert!(message.is_handshake());
assert_eq!(
message.claimed_device_id().as_deref(),
Some("device-native-token-test")
);
}
other => panic!("expected native handshake message, got {:?}", other),
}
}
#[tokio::test]
async fn inspect_rotated_session_token_responds_on_existing_connection() {
let client = Client::new_with_app_tag(
crate::test_constants::TEST_PROJECT_ID.to_string(),
"test-app".to_string(),
Box::new(|| None),
);
client.register_session_token("grant-a".into(), "share-a".into(), 1);
client.register_session_token("grant-b".into(), "share-b".into(), 1);
for (token, expected_scope, expected_first) in
[("grant-a", "share-a", true), ("grant-b", "share-b", false)]
{
let message =
crate::native_protocol::NativeMainMessage::session_token_presentation(token);
let frame = serde_json::to_vec(&message).expect("serialize token presentation");
let result = client
.inspect_incoming_native_main_frame(
"conn-token-rotation",
Some("node-rotation"),
None,
&frame,
)
.await
.expect("rotated token should receive a response");
match result {
InspectedMainFrame::SessionTokenResponse {
response,
accepted,
is_first_presentation,
} => {
assert!(accepted);
assert_eq!(is_first_presentation, expected_first);
assert_eq!(
response.approved_session_scope().as_deref(),
Some(expected_scope)
);
}
other => panic!("expected session-token response, got {other:?}"),
}
}
}
#[tokio::test]
async fn committed_token_rotation_resets_crypto_and_opposite_trust_scopes() {
let client = Client::new_with_app_tag(
crate::test_constants::TEST_PROJECT_ID.to_string(),
"test-app".to_string(),
Box::new(|| None),
);
let connection_id = "conn-security-epoch-rotation";
client
.connection_manager
.upsert_pending(connection_id.to_string(), None, None, None)
.await;
client.register_session_token("trusted-token".into(), "user-device".into(), 0);
client.register_session_token("guest-token".into(), "drive-grant:grant-1".into(), 0);
let key = [7u8; crate::application_crypto::APPLICATION_KEY_BYTES];
client.set_connection_application_crypto_key(connection_id, key);
client.set_connection_application_crypto_required(connection_id);
client.confirm_connection_application_crypto(connection_id);
client
.validate_session_token_for_connection_with_side_effects("trusted-token", connection_id)
.await
.expect("trusted token should commit");
assert_eq!(
client.connection_application_crypto_key(connection_id),
Some(key),
"the first capability must adopt a key settled during its handshake",
);
client
.protect_outbound_application_payload(connection_id, b"first")
.expect("trusted payload should encrypt");
client
.validate_session_token_for_connection_with_side_effects("trusted-token", connection_id)
.await
.expect("same token replay should remain idempotent");
assert_eq!(
client.connection_application_crypto_key(connection_id),
Some(key),
"same-token replay must preserve the application key",
);
assert_eq!(
client
.connection_application_crypto_outbound_sequences
.read()
.expect("sequence map should be readable")
.get(connection_id)
.copied(),
Some(1),
"same-token replay must preserve the outbound sequence",
);
client
.validate_session_token_for_connection_with_side_effects("guest-token", connection_id)
.await
.expect("guest token should commit on the existing connection");
assert_eq!(
client.connection_application_crypto_key(connection_id),
None
);
assert!(client.connection_requires_application_crypto(connection_id));
assert!(!client.connection_application_crypto_is_confirmed(connection_id));
assert!(!client
.connection_application_key_agreements
.read()
.expect("agreement map should be readable")
.contains_key(connection_id));
let scopes = client.connection_manager.get_scopes(connection_id).await;
assert_eq!(scopes, vec!["drive-grant:grant-1".to_string()]);
}
#[test]
fn outbound_token_rotation_fails_closed_without_resetting_same_token_state() {
let client = Client::new_with_app_tag(
crate::test_constants::TEST_PROJECT_ID.to_string(),
"test-app".to_string(),
Box::new(|| None),
);
let connection_id = "conn-outbound-security-epoch";
let key = [9u8; crate::application_crypto::APPLICATION_KEY_BYTES];
client.set_connection_application_crypto_key(connection_id, key);
client.confirm_connection_application_crypto(connection_id);
client
.connection_application_key_agreements
.write()
.expect("agreement map should be writable")
.insert(
connection_id.to_string(),
crate::key_agreement::EphemeralKeyAgreement::from_secret_bytes([3u8; 32]),
);
assert!(!client
.begin_outbound_session_token_application_security_epoch(connection_id, "token-a",));
assert!(client.connection_requires_application_crypto(connection_id));
assert_eq!(
client.connection_application_crypto_key(connection_id),
Some(key),
"the first capability must adopt a key settled during its handshake",
);
client.commit_outbound_session_token_application_security_epoch(connection_id, "token-a");
client
.protect_outbound_application_payload(connection_id, b"first")
.expect("first epoch payload should encrypt");
assert!(!client
.begin_outbound_session_token_application_security_epoch(connection_id, "token-a",));
assert_eq!(
client.connection_application_crypto_key(connection_id),
Some(key),
);
assert_eq!(
client
.connection_application_crypto_outbound_sequences
.read()
.expect("sequence map should be readable")
.get(connection_id)
.copied(),
Some(1),
);
assert!(client
.connection_application_key_agreements
.read()
.expect("agreement map should be readable")
.contains_key(connection_id));
assert!(client
.begin_outbound_session_token_application_security_epoch(connection_id, "token-b",));
assert_eq!(
client.connection_application_crypto_key(connection_id),
None
);
assert!(client.connection_requires_application_crypto(connection_id));
assert!(!client.connection_application_crypto_is_confirmed(connection_id));
assert!(!client
.connection_application_crypto_outbound_sequences
.read()
.expect("sequence map should be readable")
.contains_key(connection_id));
assert!(!client
.connection_application_key_agreements
.read()
.expect("agreement map should be readable")
.contains_key(connection_id));
}
#[tokio::test]
async fn default_admission_gate_requires_auth_from_startup() {
let client = Client::new_with_app_tag(
crate::test_constants::TEST_PROJECT_ID.to_string(),
"test-app".to_string(),
Box::new(|| None),
);
client.ensure_default_admission_gate("__startup");
assert!(client.session_registry_active());
let handshake = serde_json::json!({
"type": "handshake",
"action": "hello"
});
let mut frame = vec![0x00];
frame.extend_from_slice(handshake.to_string().as_bytes());
let err = client
.inspect_incoming_native_main_frame("conn-startup-gate", Some("node-1"), None, &frame)
.await
.expect_err("startup gate should reject tokenless handshake");
assert_eq!(err, "session-token-required");
}
#[tokio::test]
async fn managed_adoption_blocked_when_registry_active_and_not_admitted() {
let client = Client::new_with_app_tag(
crate::test_constants::TEST_PROJECT_ID.to_string(),
"test-app".to_string(),
Box::new(|| None),
);
client.register_session_token("token-1".into(), "user-device".into(), 0);
client
.connection_manager
.upsert_pending(
"conn-blocked-1".to_string(),
Some("node-blocked-1".to_string()),
None,
Some("node-blocked-1".to_string()),
)
.await;
client
.connection_manager
.set_connected_with_transport(
"conn-blocked-1",
Some("node-blocked-1".to_string()),
Some(42),
Some("incoming".to_string()),
)
.await
.expect("set connected");
let record = client
.connection_manager
.get_by_connection_id("conn-blocked-1")
.await
.expect("record exists");
let adoption = client.managed_connection_adoption(&record).await;
assert!(
adoption.is_none(),
"adoption should be blocked when registry active and connection not admitted"
);
}
#[tokio::test]
async fn managed_adoption_allowed_after_token_admission() {
let client = Client::new_with_app_tag(
crate::test_constants::TEST_PROJECT_ID.to_string(),
"test-app".to_string(),
Box::new(|| None),
);
client.register_session_token("token-adopt".into(), "user-device".into(), 0);
client
.connection_manager
.upsert_pending(
"conn-admitted-1".to_string(),
Some("node-admitted-1".to_string()),
Some("device-admitted-1".to_string()),
Some("node-admitted-1".to_string()),
)
.await;
client
.connection_manager
.set_connected_with_transport(
"conn-admitted-1",
Some("node-admitted-1".to_string()),
Some(43),
Some("incoming".to_string()),
)
.await
.expect("set connected");
client
.connection_manager
.set_device_id("conn-admitted-1", "device-admitted-1".to_string())
.await
.expect("set device_id");
client
.validate_session_token_for_connection("token-adopt", "conn-admitted-1")
.await
.expect("token should validate");
let record = client
.connection_manager
.get_by_connection_id("conn-admitted-1")
.await
.expect("record exists");
let adoption = client
.managed_connection_adoption(&record)
.await
.expect("adoption should succeed after token admission");
assert_eq!(adoption.connection_id, "conn-admitted-1");
assert_eq!(adoption.node_id, "node-admitted-1");
}
#[tokio::test]
async fn outgoing_host_approval_requires_bilateral_current_route_proof() {
let client = Client::new_with_app_tag(
crate::test_constants::TEST_PROJECT_ID.to_string(),
"test-app".to_string(),
Box::new(|| None),
);
client.register_session_token("gate-token".into(), "user-device".into(), 0);
let connection_id = "conn-host-approved-1";
let node_id = "node-host-approved-1";
let device_id = "device-host-approved-1";
client
.connection_manager
.upsert_pending(
connection_id.to_string(),
Some(node_id.to_string()),
Some(device_id.to_string()),
Some(node_id.to_string()),
)
.await;
client
.connection_manager
.set_connected_with_transport(
connection_id,
Some(node_id.to_string()),
Some(44),
Some("outgoing".to_string()),
)
.await
.expect("set connected");
client
.connection_manager
.set_device_id(connection_id, device_id.to_string())
.await
.expect("set device_id");
client
.mark_connection_admitted_by_host(
connection_id,
Some("user-device"),
Some(device_id.to_string()),
)
.await;
assert!(matches!(
client.session_admission(connection_id),
crate::session_token::SessionAdmission::Accepted { .. }
));
let record = client
.connection_manager
.get_by_connection_id(connection_id)
.await
.expect("record exists");
let adoption = client
.managed_connection_adoption(&record)
.await
.expect("host-approved outgoing connection should be adoptable");
assert_eq!(adoption.connection_id, connection_id);
assert_eq!(adoption.node_id, node_id);
assert_eq!(adoption.device_id.as_deref(), Some(device_id));
assert!(
!adoption.main_stream_ready,
"logical host approval must not bypass physical route admission"
);
establish_current_managed_user_device_route(&client, connection_id, 44);
let adoption = client
.managed_connection_adoption(&record)
.await
.expect("current bilateral route should remain adoptable");
assert!(adoption.main_stream_ready);
}
#[tokio::test]
async fn outgoing_host_approval_binds_identity_without_bypassing_route_proof() {
let client = Client::new_with_app_tag(
crate::test_constants::TEST_PROJECT_ID.to_string(),
"test-app".to_string(),
Box::new(|| None),
);
client.register_session_token("gate-token".into(), "user-device".into(), 0);
let connection_id = "conn-host-approved-no-prebind";
let node_id = "node-host-approved-no-prebind";
let device_id = "device-host-approved-target";
client
.connection_manager
.upsert_pending(
connection_id.to_string(),
Some(node_id.to_string()),
None,
Some(node_id.to_string()),
)
.await;
client
.connection_manager
.set_connected_with_transport(
connection_id,
Some(node_id.to_string()),
Some(45),
Some("outgoing".to_string()),
)
.await
.expect("set connected");
client
.mark_connection_admitted_by_host(
connection_id,
Some("user-device"),
Some(device_id.to_string()),
)
.await;
let record = client
.connection_manager
.get_by_connection_id(connection_id)
.await
.expect("record exists");
assert_eq!(record.device_id.as_deref(), Some(device_id));
let adoption = client
.managed_connection_adoption(&record)
.await
.expect("host-approved outgoing connection should be adoptable");
assert_eq!(adoption.device_id.as_deref(), Some(device_id));
assert!(
!adoption.main_stream_ready,
"authoritative identity binding must not admit an unproven route"
);
establish_current_managed_user_device_route(&client, connection_id, 45);
let adoption = client
.managed_connection_adoption(&record)
.await
.expect("current bilateral route should remain adoptable");
assert!(adoption.main_stream_ready);
}
#[cfg(all(feature = "transport-webrtc", not(target_arch = "wasm32")))]
#[tokio::test]
async fn accepting_replacement_peer_retires_stale_connection_for_same_device() {
let client = Client::new_with_app_tag(
crate::test_constants::TEST_PROJECT_ID.to_string(),
"test-app".to_string(),
Box::new(|| None),
);
client
.connection_manager
.upsert_pending(
"conn-old".to_string(),
Some("node-old".to_string()),
Some("device-a".to_string()),
Some("node-old".to_string()),
)
.await;
client
.connection_manager
.set_connected_with_transport(
"conn-old",
Some("node-old".to_string()),
Some(41),
Some("incoming".to_string()),
)
.await
.expect("set old connected");
client
.connection_manager
.set_device_id("conn-old", "device-a".to_string())
.await
.expect("set old device");
client
.connection_manager
.upsert_pending(
"conn-new".to_string(),
Some("node-new".to_string()),
Some("device-a".to_string()),
Some("node-new".to_string()),
)
.await;
client
.connection_manager
.set_connected_with_transport(
"conn-new",
Some("node-new".to_string()),
Some(42),
Some("incoming".to_string()),
)
.await
.expect("set new connected");
client
.connection_manager
.set_device_id("conn-new", "device-a".to_string())
.await
.expect("set new device");
client
.native_webrtc_attempt_counts
.write()
.await
.insert("conn-old".to_string(), 5);
client.native_webrtc_suppressions.write().await.insert(
"conn-old".to_string(),
crate::client::NativeWebRTCSuppression {
until_ms: i64::MAX,
reason: "upgrade-attempts-exhausted".to_string(),
},
);
client
.accept_replacement_peer("conn-new", Some("device-a"), "replacement-peer-admitted")
.await;
assert!(client
.connection_manager
.get_by_connection_id("conn-old")
.await
.is_none());
assert!(client
.connection_manager
.get_by_connection_id("conn-new")
.await
.is_some());
assert!(client
.native_webrtc_attempt_counts
.read()
.await
.get("conn-old")
.is_none());
assert!(client
.native_webrtc_suppressions
.read()
.await
.get("conn-old")
.is_none());
}
#[tokio::test]
async fn native_user_device_ticket_cache_reuses_backend_token_within_app_session() {
let client = Client::new_with_app_tag(
crate::test_constants::TEST_PROJECT_ID.to_string(),
"test-app".to_string(),
Box::new(|| None),
);
let base_dir = std::env::temp_dir().join(format!(
"openrtc-user-device-cache-{}",
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.expect("unix epoch")
.as_nanos()
));
client
.init_native_device_identity(base_dir, Some("Managed Cache Device"))
.await
.expect("init native device identity");
client.init_iroh(None, Vec::new()).await.expect("init iroh");
let first = client
.endpoint_ticket_with_token("user-device", 0)
.await
.expect("first managed ticket");
let second = client
.endpoint_ticket_with_token("user-device", 0)
.await
.expect("second managed ticket");
let (_, first_suffix) = crate::session_token::split_compound_ticket(&first);
let (_, second_suffix) = crate::session_token::split_compound_ticket(&second);
let first_payload =
crate::session_token::decode_token_payload(first_suffix.expect("first token suffix"))
.expect("first payload");
let second_payload =
crate::session_token::decode_token_payload(second_suffix.expect("second token suffix"))
.expect("second payload");
assert_eq!(first_payload.scope, "user-device");
assert_eq!(second_payload.scope, "user-device");
assert_eq!(
first_payload.token, second_payload.token,
"native managed ticket cache should reuse the same backend-owned token within one app session"
);
}
#[tokio::test]
async fn current_node_id_repairs_a_stale_projection_from_the_live_endpoint() {
let client = Client::new_with_app_tag(
crate::test_constants::TEST_PROJECT_ID.to_string(),
"node-authority-test".to_string(),
Box::new(|| None),
);
let endpoint = iroh::Endpoint::builder(iroh::endpoint::presets::Minimal)
.bind()
.await
.expect("bind local endpoint");
let endpoint_node_id = endpoint.id().to_string();
*client.iroh_endpoint.write().await = Some(endpoint.clone());
*client.node_id.write().await = Some("stale-node-projection".to_string());
assert_eq!(
client.current_node_id().await.as_deref(),
Some(endpoint_node_id.as_str())
);
assert_eq!(
client.node_id.read().await.as_deref(),
Some(endpoint_node_id.as_str())
);
endpoint.close().await;
}
#[tokio::test]
async fn adopting_a_second_native_endpoint_never_replaces_the_lifecycle_owner() {
let client = Client::new_with_app_tag(
crate::test_constants::TEST_PROJECT_ID.to_string(),
"endpoint-adoption-owner-test".to_string(),
Box::new(|| None),
);
let first = iroh::Endpoint::builder(iroh::endpoint::presets::Minimal)
.alpns(vec![crate::native_node::PlutoniumProtocol::ALPN.to_vec()])
.bind()
.await
.expect("bind first endpoint");
let first_node_id = first.id().to_string();
client
.adopt_endpoint_with_router_mode(first, false)
.await
.expect("adopt first endpoint");
let second = iroh::Endpoint::builder(iroh::endpoint::presets::Minimal)
.alpns(vec![crate::native_node::PlutoniumProtocol::ALPN.to_vec()])
.bind()
.await
.expect("bind second endpoint");
let second_node_id = second.id().to_string();
let error = client
.adopt_endpoint_with_router_mode(second, false)
.await
.expect_err("a live endpoint must not be replaced");
assert!(error.to_string().contains("refusing replacement"));
assert!(error.to_string().contains(&first_node_id));
assert!(error.to_string().contains(&second_node_id));
assert_eq!(
client.current_node_id().await.as_deref(),
Some(first_node_id.as_str())
);
assert_eq!(
client
.get_endpoint()
.await
.expect("current endpoint")
.id()
.to_string(),
first_node_id
);
client
.get_endpoint()
.await
.expect("current endpoint")
.close()
.await;
}
#[tokio::test]
async fn concurrent_native_endpoint_adoptions_publish_exactly_one_owner() {
let client = Client::new_with_app_tag(
crate::test_constants::TEST_PROJECT_ID.to_string(),
"concurrent-endpoint-adoption-test".to_string(),
Box::new(|| None),
);
let first = iroh::Endpoint::builder(iroh::endpoint::presets::Minimal)
.alpns(vec![crate::native_node::PlutoniumProtocol::ALPN.to_vec()])
.bind()
.await
.expect("bind first endpoint");
let second = iroh::Endpoint::builder(iroh::endpoint::presets::Minimal)
.alpns(vec![crate::native_node::PlutoniumProtocol::ALPN.to_vec()])
.bind()
.await
.expect("bind second endpoint");
let (first_result, second_result) = tokio::join!(
client.adopt_endpoint_with_router_mode(first, false),
client.adopt_endpoint_with_router_mode(second, false),
);
assert_ne!(
first_result.is_ok(),
second_result.is_ok(),
"exactly one endpoint candidate must win initialization"
);
let winner = first_result.or(second_result).expect("one endpoint wins");
assert_eq!(
client.current_node_id().await.as_deref(),
Some(winner.as_str())
);
client
.get_endpoint()
.await
.expect("winning endpoint")
.close()
.await;
}
#[tokio::test]
async fn native_user_device_ticket_persists_across_app_restarts() {
let base_dir = std::env::temp_dir().join(format!(
"openrtc-user-device-persist-{}",
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.expect("unix epoch")
.as_nanos()
));
let client_a = Client::new_with_app_tag(
crate::test_constants::TEST_PROJECT_ID.to_string(),
"test-app".to_string(),
Box::new(|| None),
);
client_a
.init_native_device_identity(base_dir.clone(), None)
.await
.expect("init native device identity a");
client_a
.init_iroh(None, Vec::new())
.await
.expect("init iroh a");
let first = client_a
.endpoint_ticket_with_token("user-device", 0)
.await
.expect("first managed ticket");
let client_b = Client::new_with_app_tag(
crate::test_constants::TEST_PROJECT_ID.to_string(),
"test-app".to_string(),
Box::new(|| None),
);
client_b
.init_native_device_identity(base_dir.clone(), None)
.await
.expect("init native device identity b");
client_b
.init_iroh(None, Vec::new())
.await
.expect("init iroh b");
let second = client_b
.endpoint_ticket_with_token("user-device", 0)
.await
.expect("second managed ticket");
let (_, first_suffix) = crate::session_token::split_compound_ticket(&first);
let (_, second_suffix) = crate::session_token::split_compound_ticket(&second);
let first_payload =
crate::session_token::decode_token_payload(first_suffix.expect("first token suffix"))
.expect("first payload");
let second_payload =
crate::session_token::decode_token_payload(second_suffix.expect("second token suffix"))
.expect("second payload");
assert_eq!(first_payload.token, second_payload.token);
let _ = tokio::fs::remove_dir_all(&base_dir).await;
}
#[tokio::test]
async fn concurrent_native_identity_initialization_is_single_flight() {
let base_dir = std::env::temp_dir().join(format!(
"openrtc-device-init-single-flight-{}",
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.expect("unix epoch")
.as_nanos()
));
let client = std::sync::Arc::new(Client::new_with_app_tag(
crate::test_constants::TEST_PROJECT_ID.to_string(),
"test-app".to_string(),
Box::new(|| None),
));
let start = std::sync::Arc::new(tokio::sync::Barrier::new(17));
let mut tasks = Vec::new();
for index in 0..16 {
let client = client.clone();
let base_dir = base_dir.clone();
let start = start.clone();
tasks.push(tokio::spawn(async move {
start.wait().await;
client
.init_native_device_identity(
base_dir,
Some(&format!("Concurrent Device {index}")),
)
.await
.expect("initialize native identity")
}));
}
start.wait().await;
let mut identities = Vec::new();
for task in tasks {
identities.push(task.await.expect("identity task"));
}
let expected_id = identities[0].device_id.clone();
assert!(identities
.iter()
.all(|identity| identity.device_id == expected_id));
assert_eq!(
client
.get_native_device_identity()
.await
.expect("cached native identity")
.device_id,
expected_id
);
assert_eq!(
crate::native_device::load_or_create(&base_dir, None)
.await
.expect("persisted native identity")
.device_id,
expected_id
);
let _ = tokio::fs::remove_dir_all(&base_dir).await;
}
#[tokio::test]
async fn revoking_persistent_user_device_scope_rotates_next_native_ticket() {
let base_dir = std::env::temp_dir().join(format!(
"openrtc-user-device-rotate-{}",
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.expect("unix epoch")
.as_nanos()
));
let client = Client::new_with_app_tag(
crate::test_constants::TEST_PROJECT_ID.to_string(),
"test-app".to_string(),
Box::new(|| None),
);
client
.init_native_device_identity(base_dir.clone(), None)
.await
.expect("init native device identity");
client.init_iroh(None, Vec::new()).await.expect("init iroh");
let first = client
.endpoint_ticket_with_token("user-device", 0)
.await
.expect("first managed ticket");
client.revoke_tokens_by_scope("user-device").await;
let second = client
.endpoint_ticket_with_token("user-device", 0)
.await
.expect("second managed ticket");
let (_, first_suffix) = crate::session_token::split_compound_ticket(&first);
let (_, second_suffix) = crate::session_token::split_compound_ticket(&second);
let first_payload =
crate::session_token::decode_token_payload(first_suffix.expect("first token suffix"))
.expect("first payload");
let second_payload =
crate::session_token::decode_token_payload(second_suffix.expect("second token suffix"))
.expect("second payload");
assert_ne!(first_payload.token, second_payload.token);
let _ = tokio::fs::remove_dir_all(&base_dir).await;
}
#[tokio::test]
async fn revoking_scope_retires_remote_host_approval_from_admission_authority() {
let client = Client::new_with_app_tag(
crate::test_constants::TEST_PROJECT_ID.to_string(),
"test-app".to_string(),
Box::new(|| None),
);
let connection_id = "remote-host-approved-user-device";
client
.connection_manager
.upsert_pending(connection_id.to_string(), None, None, None)
.await;
client
.connection_manager
.set_connecting(connection_id)
.await;
client
.connection_manager
.set_connected(connection_id, None)
.await;
client.session_token_registry.mark_accepted(
connection_id,
crate::session_token::SessionAdmissionMechanism::SessionToken,
Some(crate::session_token::GrantScope::from("user-device")),
None,
);
client
.native_route_repair_credentials
.write()
.expect("route repair credentials")
.insert(
"remote-node".to_string(),
crate::client::NativeRouteRepairCredential {
token: "remote-route-token".to_string(),
token_payload: "remote-route-payload".to_string(),
authoritative_device_id: "remote-device".to_string(),
},
);
let affected = client.revoke_tokens_by_scope("user-device").await;
assert_eq!(affected, vec![connection_id.to_string()]);
assert_eq!(
client
.connection_manager
.get_by_connection_id(connection_id)
.await
.expect("connection record")
.state,
ConnectionState::Closed,
);
assert!(
client
.native_route_repair_credentials
.read()
.expect("route repair credentials")
.is_empty(),
"scope revocation must discard cached route-repair credentials"
);
}
#[test]
fn revoking_session_token_discards_matching_route_repair_credential() {
let client = Client::new_with_app_tag(
crate::test_constants::TEST_PROJECT_ID.to_string(),
"test-app".to_string(),
Box::new(|| None),
);
client
.native_route_repair_credentials
.write()
.expect("route repair credentials")
.insert(
"remote-node".to_string(),
crate::client::NativeRouteRepairCredential {
token: "revoked-route-token".to_string(),
token_payload: "remote-route-payload".to_string(),
authoritative_device_id: "remote-device".to_string(),
},
);
client.revoke_session_token("revoked-route-token");
assert!(
client
.native_route_repair_credentials
.read()
.expect("route repair credentials")
.is_empty(),
"token revocation must discard its cached route-repair credential"
);
}
#[tokio::test]
async fn user_device_tokens_allow_multiple_connection_admissions_when_unlimited() {
let client = Client::new_with_app_tag(
crate::test_constants::TEST_PROJECT_ID.to_string(),
"test-app".to_string(),
Box::new(|| None),
);
client.register_session_token("user-device-token".into(), "user-device".into(), 0);
let first_scope = client
.validate_session_token_for_connection("user-device-token", "conn-user-device-1")
.await
.expect("first connection should be admitted");
let second_scope = client
.validate_session_token_for_connection("user-device-token", "conn-user-device-2")
.await
.expect("second connection should be admitted");
assert_eq!(first_scope, "user-device");
assert_eq!(second_scope, "user-device");
assert!(matches!(
client.session_admission("conn-user-device-1"),
crate::session_token::SessionAdmission::Accepted { .. }
));
assert!(matches!(
client.session_admission("conn-user-device-2"),
crate::session_token::SessionAdmission::Accepted { .. }
));
}
#[test]
fn compound_ticket_splits_correctly_for_user_device_scope() {
let iroh_ticket = "endpointABCDEF1234567890";
let token = "test-user-device-token-abc";
let compound =
crate::session_token::build_compound_ticket(iroh_ticket, token, "user-device", 0);
let (recovered_ticket, suffix) = crate::session_token::split_compound_ticket(&compound);
assert_eq!(recovered_ticket, iroh_ticket);
assert!(suffix.is_some());
let payload = crate::session_token::decode_token_payload(suffix.unwrap())
.expect("token payload should decode");
assert_eq!(payload.token, token);
assert_eq!(payload.scope, "user-device");
assert_eq!(payload.max_connections, 0);
}
#[tokio::test]
async fn deterministic_connection_id_is_symmetric() {
let id_ab = Client::deterministic_connection_id("node-aaa", "node-zzz");
let id_ba = Client::deterministic_connection_id("node-zzz", "node-aaa");
assert_eq!(
id_ab, id_ba,
"connection_id must be identical regardless of dial direction"
);
}
#[tokio::test]
async fn rejection_reset_allows_fresh_admission_after_timeout() {
let client = Client::new_with_app_tag(
crate::test_constants::TEST_PROJECT_ID.to_string(),
"test-app".to_string(),
Box::new(|| None),
);
client.register_session_token("ud-token".into(), "user-device".into(), 0);
let connection_id = "conn-rejection-reset";
client.reject_session_connection(connection_id, "session-admission-timeout");
assert!(
matches!(
client.session_admission(connection_id),
crate::session_token::SessionAdmission::Rejected { .. }
),
"connection should be rejected after timeout"
);
client.forget_session_connection(connection_id);
assert!(
matches!(
client.session_admission(connection_id),
crate::session_token::SessionAdmission::Pending
),
"connection should be Pending after forget, got {:?}",
client.session_admission(connection_id)
);
let scope = client
.validate_session_token_for_connection("ud-token", connection_id)
.await
.expect("token should validate after rejection reset");
assert_eq!(scope, "user-device");
assert!(matches!(
client.session_admission(connection_id),
crate::session_token::SessionAdmission::Accepted { .. }
));
}
#[cfg(not(target_arch = "wasm32"))]
#[tokio::test]
async fn native_peer_snapshot_read_does_not_retire_transiently_missing_transport() {
let client = Client::new_with_app_tag(
crate::test_constants::TEST_PROJECT_ID.to_string(),
"test-app".to_string(),
Box::new(|| None),
);
let endpoint = Endpoint::builder(iroh::endpoint::presets::N0)
.relay_mode(RelayMode::Disabled)
.bind_addr(
"127.0.0.1:0"
.parse::<std::net::SocketAddr>()
.expect("socket addr"),
)
.expect("bind addr")
.bind()
.await
.expect("bind endpoint");
let remote_node_id = endpoint.id().to_string();
let connection_id = "conn-transient-path-refresh";
client
.connection_manager
.upsert_pending(
connection_id.to_string(),
Some(remote_node_id.clone()),
Some("device-transient-path-refresh".to_string()),
Some(remote_node_id),
)
.await;
client
.connection_manager
.set_connected_with_transport(
connection_id,
None,
Some(42),
Some("native-quic".to_string()),
)
.await
.expect("connected record");
let snapshot = client
.peer_snapshot(connection_id)
.await
.expect("read-side projection must preserve the peer");
assert_eq!(snapshot.connection_ids, vec![connection_id.to_string()]);
assert!(
client
.connection_manager
.get_by_connection_id(connection_id)
.await
.is_some(),
"native read path must leave retirement to thresholded health reconciliation"
);
endpoint.close().await;
}
#[tokio::test]
async fn tie_break_both_peers_share_deterministic_connection_id() {
let client_a = Client::new_with_app_tag(
crate::test_constants::TEST_PROJECT_ID.to_string(),
"test-app".to_string(),
Box::new(|| None),
);
client_a.register_session_token("host-token".into(), "user-device".into(), 0);
let node_a = "aaaa1111aaaa1111aaaa1111aaaa1111";
let node_b = "zzzz9999zzzz9999zzzz9999zzzz9999";
let connection_id = Client::deterministic_connection_id(node_a, node_b);
let connection_id_reverse = Client::deterministic_connection_id(node_b, node_a);
assert_eq!(connection_id, connection_id_reverse);
client_a.reject_session_connection(&connection_id, "session-admission-timeout");
client_a.forget_session_connection(&connection_id);
let scope = client_a
.validate_session_token_for_connection("host-token", &connection_id)
.await
.expect("token should be accepted after rejection reset");
assert_eq!(scope, "user-device");
}
#[tokio::test]
async fn managed_adoption_uses_node_id_fallback_for_share_scope_anonymous_peer() {
let client = Client::new_with_app_tag(
crate::test_constants::TEST_PROJECT_ID.to_string(),
"test-app".to_string(),
Box::new(|| None),
);
client.register_session_token("anon-token".into(), "share".into(), 0);
let connection_id = "conn-anon-peer";
let node_id = "node-anon-peer-1234";
client
.validate_session_token_for_connection("anon-token", connection_id)
.await
.expect("token should validate");
client.bind_inbound_native_admission_stream_contract(
connection_id,
crate::native_protocol::SessionTokenStreamContract::OneShotAdmission,
);
client
.connection_manager
.upsert_pending(
connection_id.to_string(),
Some(node_id.to_string()),
None, Some(node_id.to_string()),
)
.await;
client
.connection_manager
.set_connected_with_transport(
connection_id,
Some(node_id.to_string()),
Some(99),
Some("managed".to_string()),
)
.await
.expect("set connected");
client.bind_inbound_session_token_transport(connection_id, 99);
let record = client
.connection_manager
.get_by_connection_id(connection_id)
.await
.expect("record exists");
let adoption = client
.managed_connection_adoption(&record)
.await
.expect("adoption should succeed with node_id fallback");
assert_eq!(adoption.connection_id, connection_id);
assert_eq!(adoption.node_id, node_id);
assert_eq!(
adoption.device_id.as_deref(),
Some(node_id),
"device_id should fall back to node_id for anonymous share peers"
);
assert!(
adoption.main_stream_ready,
"main_stream_ready should be true when device_id fallback is used"
);
}
#[tokio::test]
async fn transient_managed_adoption_ignores_stale_durable_device_identity() {
let client = Client::new_with_app_tag(
crate::test_constants::TEST_PROJECT_ID.to_string(),
"test-app".to_string(),
Box::new(|| None),
);
let connection_id = "conn-transient-stale-device";
let node_id = "node-transient-current";
let stale_device_id = "device-from-prior-user-session";
client.register_session_token("transient-token".into(), "magic-link".into(), 1);
client
.validate_session_token_for_connection("transient-token", connection_id)
.await
.expect("transient token should validate");
client.bind_inbound_native_admission_stream_contract(
connection_id,
crate::native_protocol::SessionTokenStreamContract::OneShotAdmission,
);
client
.connection_manager
.upsert_pending(
connection_id.to_string(),
Some(node_id.to_string()),
Some(stale_device_id.to_string()),
Some(node_id.to_string()),
)
.await;
client
.connection_manager
.set_connected_with_transport(
connection_id,
Some(node_id.to_string()),
Some(101),
Some("managed".to_string()),
)
.await
.expect("set connected");
client.bind_inbound_session_token_transport(connection_id, 101);
let record = client
.connection_manager
.get_by_connection_id(connection_id)
.await
.expect("record exists");
let adoption = client
.managed_connection_adoption(&record)
.await
.expect("transient route should be adoptable");
assert_eq!(adoption.device_id.as_deref(), Some(node_id));
assert_ne!(adoption.device_id.as_deref(), Some(stale_device_id));
}
#[tokio::test]
async fn managed_adoption_uses_node_id_for_current_drive_grant_route() {
let client = Client::new_with_app_tag(
crate::test_constants::TEST_PROJECT_ID.to_string(),
"test-app".to_string(),
Box::new(|| None),
);
let connection_id = "conn-drive-grant-guest";
let node_id = "node-drive-grant-guest";
let transport_stable_id = 119;
let scope = "drive-grant:test-grant";
client.register_session_token("drive-grant-token".into(), scope.into(), 1);
client
.validate_session_token_for_connection("drive-grant-token", connection_id)
.await
.expect("drive grant token should validate");
client.bind_outbound_native_admission_stream_contract(
connection_id,
crate::native_protocol::SessionTokenStreamContract::OneShotAdmission,
);
client
.remote_session_admission_proofs
.write()
.expect("remote proof registry")
.insert(
connection_id.to_string(),
RemoteSessionAdmissionProof {
transport_stable_id,
token_fingerprint: "drive-grant-proof".to_string(),
approval_scope: scope.to_string(),
},
);
client
.connection_manager
.upsert_pending(
connection_id.to_string(),
Some(node_id.to_string()),
None,
Some(node_id.to_string()),
)
.await;
client
.connection_manager
.set_connected_with_transport(
connection_id,
Some(node_id.to_string()),
Some(transport_stable_id),
Some("managed".to_string()),
)
.await
.expect("set connected");
let record = client
.connection_manager
.get_by_connection_id(connection_id)
.await
.expect("record exists");
let adoption = client
.managed_connection_adoption(&record)
.await
.expect("admitted drive grant should be adoptable");
assert_eq!(adoption.node_id, node_id);
assert_eq!(
adoption.device_id.as_deref(),
Some(node_id),
"transient grant adoption uses node identity without durable device binding",
);
assert!(
adoption.main_stream_ready,
"a current one-shot approval must make the drive grant route ready",
);
assert_eq!(
client
.connection_manager
.get_by_connection_id(connection_id)
.await
.and_then(|record| record.device_id),
None,
"transient adoption must not mutate durable device identity",
);
}
#[tokio::test]
async fn managed_adoption_requires_real_device_identity_for_user_device_scope() {
let client = Client::new_with_app_tag(
crate::test_constants::TEST_PROJECT_ID.to_string(),
"test-app".to_string(),
Box::new(|| None),
);
client.register_session_token("user-device-token".into(), "user-device".into(), 0);
let connection_id = "conn-user-device-anon";
let node_id = "node-user-device-anon";
client
.validate_session_token_for_connection("user-device-token", connection_id)
.await
.expect("token should validate");
client
.connection_manager
.upsert_pending(
connection_id.to_string(),
Some(node_id.to_string()),
None,
Some(node_id.to_string()),
)
.await;
client
.connection_manager
.set_connected_with_transport(
connection_id,
Some(node_id.to_string()),
Some(109),
Some("managed".to_string()),
)
.await
.expect("set connected");
let record = client
.connection_manager
.get_by_connection_id(connection_id)
.await
.expect("record exists");
let adoption = client
.managed_connection_adoption(&record)
.await
.expect("adoption snapshot should still be produced");
assert_eq!(adoption.connection_id, connection_id);
assert_eq!(adoption.node_id, node_id);
assert_eq!(
adoption.device_id, None,
"user-device scope must not fall back to node_id as app identity"
);
assert!(
!adoption.main_stream_ready,
"user-device adoption should wait for an authoritative device identity"
);
}
#[tokio::test]
async fn managed_adoption_blocked_for_unadmitted_peer_even_with_device_id() {
let client = Client::new_with_app_tag(
crate::test_constants::TEST_PROJECT_ID.to_string(),
"test-app".to_string(),
Box::new(|| None),
);
client.register_session_token("gate-token".into(), "user-device".into(), 0);
let connection_id = "conn-unadmitted-with-device";
let node_id = "node-unadmitted";
client
.connection_manager
.upsert_pending(
connection_id.to_string(),
Some(node_id.to_string()),
Some("real-device-id".to_string()),
Some(node_id.to_string()),
)
.await;
client
.connection_manager
.set_connected_with_transport(
connection_id,
Some(node_id.to_string()),
Some(50),
Some("incoming".to_string()),
)
.await
.expect("set connected");
client
.connection_manager
.set_device_id(connection_id, "real-device-id".to_string())
.await
.expect("set device_id");
let record = client
.connection_manager
.get_by_connection_id(connection_id)
.await
.expect("record exists");
let adoption = client.managed_connection_adoption(&record).await;
assert!(
adoption.is_none(),
"unadmitted connection should not be adoptable even with device_id"
);
}
#[tokio::test]
async fn late_device_binding_completes_user_device_session_admission() {
let client = Client::new_with_app_tag(
crate::test_constants::TEST_PROJECT_ID.to_string(),
"test-app".to_string(),
Box::new(|| None),
);
let connection_id = "conn-late-authoritative-device";
let device_id = "0807631e-97af-4fb1-b245-93a63ff4dcad";
client
.connection_manager
.upsert_pending(
connection_id.to_string(),
Some("node-1".to_string()),
None,
None,
)
.await;
client
.mark_connection_admitted_by_host(connection_id, Some("user-device"), None)
.await;
assert!(matches!(
client.session_admission(connection_id),
crate::session_token::SessionAdmission::Accepted {
scope: Some(_),
authoritative_device_id: None,
..
}
));
assert!(
client
.bind_session_admission_authoritative_device_id(connection_id, device_id)
.await,
"late binding should fill the missing authoritative device id"
);
assert!(matches!(
client.session_admission(connection_id),
crate::session_token::SessionAdmission::Accepted {
scope: Some(scope),
authoritative_device_id: Some(bound),
..
} if scope.as_str() == "user-device" && bound == device_id
));
let record = client
.connection_manager
.get_by_connection_id(connection_id)
.await
.expect("managed connection record should remain indexed");
assert_eq!(record.device_id.as_deref(), Some(device_id));
}
#[tokio::test]
async fn bind_connection_device_id_preserves_managed_user_device_admission() {
let client = Client::new_with_app_tag(
crate::test_constants::TEST_PROJECT_ID.to_string(),
"test-app".to_string(),
Box::new(|| None),
);
client.register_session_token("active-gate-token".into(), "user-device".into(), 0);
let connection_id = "conn-bound-managed-user-device";
let device_id = "managed-device-bound-from-handshake";
let node_id = "node-bound-managed-user-device";
client
.connection_manager
.upsert_pending(
connection_id.to_string(),
Some(node_id.to_string()),
None,
Some(node_id.to_string()),
)
.await;
client
.connection_manager
.set_connected_with_transport(
connection_id,
Some("node-bound-managed-user-device".to_string()),
Some(123),
Some("wasm-accept".to_string()),
)
.await
.expect("set connected");
client
.mark_connection_admitted_by_host(
connection_id,
Some("user-device"),
Some(device_id.to_string()),
)
.await;
establish_current_managed_user_device_route(&client, connection_id, 123);
client
.connection_manager
.set_health(connection_id, ConnectionHealth::Healthy)
.await;
let snapshot = client
.bind_connection_device_id(connection_id, device_id)
.await
.expect("bound peer snapshot");
assert_eq!(snapshot.peer_id, device_id);
assert!(snapshot.scopes.iter().any(|scope| scope == "user-device"));
assert!(matches!(
client.session_admission(connection_id),
crate::session_token::SessionAdmission::Accepted {
mechanism: crate::session_token::SessionAdmissionMechanism::SessionToken,
scope: Some(scope),
authoritative_device_id: Some(bound),
} if scope.as_str() == "user-device" && bound == device_id
));
let session = client
.peer_session(device_id)
.await
.expect("peer session snapshot");
assert_eq!(session.peer_id, device_id);
assert!(session.settled_ready);
assert_eq!(session.readiness_reason, "transport-healthy");
assert!(session.scopes.iter().any(|scope| scope == "user-device"));
client.connection_manager.remove(connection_id).await;
assert_eq!(
client
.known_remote_device_id_for_incoming_transport(connection_id, node_id)
.await
.as_deref(),
Some(device_id),
"logical identity must survive retirement of the physical connection record"
);
let replacement_node_id = "node-bound-managed-user-device-replacement";
client
.reconcile_authoritative_device_node(device_id, replacement_node_id)
.await;
assert_eq!(
client
.known_remote_device_id_for_incoming_transport(connection_id, replacement_node_id)
.await
.as_deref(),
Some(device_id),
"the current physical node must retain the durable identity"
);
assert!(
client
.known_remote_device_id_for_incoming_transport(connection_id, node_id)
.await
.is_none(),
"an endpoint replaced by the same durable device must lose its alias"
);
}
#[tokio::test]
async fn bind_node_device_id_reconciles_an_accept_before_discovery() {
let client = Client::new_with_app_tag(
crate::test_constants::TEST_PROJECT_ID.to_string(),
"test-app".to_string(),
Box::new(|| None),
);
client.register_session_token("active-gate-token".into(), "user-device".into(), 0);
let connection_id = "conn-accepted-before-directory";
let node_id = "node-accepted-before-directory";
let device_id = "device-discovered-after-accept";
client
.connection_manager
.upsert_pending(
connection_id.to_string(),
Some(node_id.to_string()),
None,
Some(node_id.to_string()),
)
.await;
client
.connection_manager
.set_connected_with_transport(
connection_id,
Some(node_id.to_string()),
Some(456),
Some("wasm-accept".to_string()),
)
.await
.expect("set connected");
client
.mark_connection_admitted_by_host(connection_id, Some("user-device"), None)
.await;
assert!(client.peer_session(device_id).await.is_none());
client.bind_node_device_id(node_id, device_id).await;
let record = client
.connection_manager
.get_by_connection_id(connection_id)
.await
.expect("managed record remains present");
assert_eq!(record.device_id.as_deref(), Some(device_id));
assert!(client.peer_session(device_id).await.is_some());
}
#[tokio::test]
async fn bind_node_device_id_preserves_discovery_before_accept() {
let client = Client::new_with_app_tag(
crate::test_constants::TEST_PROJECT_ID.to_string(),
"test-app".to_string(),
Box::new(|| None),
);
let node_id = "node-discovered-before-accept";
let device_id = "device-discovered-before-accept";
client.bind_node_device_id(node_id, device_id).await;
assert_eq!(
client
.known_remote_device_id_for_incoming_transport("future-connection", node_id)
.await
.as_deref(),
Some(device_id),
);
}
#[test]
fn authoritative_device_node_observation_replaces_the_retired_alias_synchronously() {
let client = Client::new_with_app_tag(
crate::test_constants::TEST_PROJECT_ID.to_string(),
"test-app".to_string(),
Box::new(|| None),
);
client.observe_authoritative_device_node("device-1", "node-old");
assert_eq!(
client.authoritative_node_for_device("device-1").as_deref(),
Some("node-old")
);
client.observe_authoritative_device_node("device-1", "node-new");
assert_eq!(
client.authoritative_node_for_device("device-1").as_deref(),
Some("node-new")
);
assert_eq!(
client
.known_device_ids_by_node
.read()
.expect("known device aliases")
.get("node-old"),
None
);
}
#[tokio::test]
async fn bind_connection_device_id_refuses_transient_session_admission() {
let client = Client::new_with_app_tag(
crate::test_constants::TEST_PROJECT_ID.to_string(),
"test-app".to_string(),
Box::new(|| None),
);
client.register_session_token("share-token".into(), "share:session-1".into(), 0);
let connection_id = "conn-transient-share";
let device_id = "transient-generated-device";
let node_id = "node-transient-share";
client
.connection_manager
.upsert_pending(
connection_id.to_string(),
Some(node_id.to_string()),
None,
Some(node_id.to_string()),
)
.await;
client
.mark_connection_admitted_by_host(connection_id, Some("share:session-1"), None)
.await;
let snapshot = client
.bind_connection_device_id(connection_id, device_id)
.await
.expect("transient peer snapshot remains available");
assert_ne!(snapshot.peer_id, device_id);
assert_eq!(snapshot.device_id, None);
assert!(snapshot
.scopes
.iter()
.any(|scope| scope == "share:session-1"));
assert!(!snapshot.scopes.iter().any(|scope| scope == "user-device"));
assert!(matches!(
client.session_admission(connection_id),
crate::session_token::SessionAdmission::Accepted {
scope: Some(scope),
authoritative_device_id: None,
..
} if scope.as_str() == "share:session-1"
));
}
#[tokio::test]
async fn bind_connection_device_id_does_not_promote_pending_transient_session() {
let client = Client::new_with_app_tag(
crate::test_constants::TEST_PROJECT_ID.to_string(),
"test-app".to_string(),
Box::new(|| None),
);
client.register_session_token("share-token".into(), "share:session-race".into(), 0);
let connection_id = "conn-pending-transient-share";
let device_id = "transient-generated-device-before-admission";
client
.connection_manager
.upsert_pending(
connection_id.to_string(),
Some("node-pending-transient-share".to_string()),
None,
None,
)
.await;
let pending_snapshot = client
.bind_connection_device_id(connection_id, device_id)
.await
.expect("pending peer snapshot remains available");
assert_eq!(pending_snapshot.device_id.as_deref(), Some(device_id));
assert!(!pending_snapshot
.scopes
.iter()
.any(|scope| scope == "user-device"));
assert!(
!client
.mark_trusted_user_device_connection_admitted(connection_id, device_id)
.await
);
assert!(matches!(
client.session_admission(connection_id),
crate::session_token::SessionAdmission::Pending
));
client
.mark_connection_admitted_by_host(connection_id, Some("share:session-race"), None)
.await;
let admitted_snapshot = client
.connection_manager
.peer_snapshot(connection_id)
.await
.expect("admitted transient peer snapshot remains available");
assert!(admitted_snapshot
.scopes
.iter()
.any(|scope| scope == "share:session-race"));
assert!(!admitted_snapshot
.scopes
.iter()
.any(|scope| scope == "user-device"));
assert!(matches!(
client.session_admission(connection_id),
crate::session_token::SessionAdmission::Accepted {
scope: Some(scope),
..
} if scope.as_str() == "share:session-race"
));
}
#[tokio::test]
async fn user_device_token_survives_multiple_rejection_reset_cycles() {
let client = Client::new_with_app_tag(
crate::test_constants::TEST_PROJECT_ID.to_string(),
"test-app".to_string(),
Box::new(|| None),
);
client.register_session_token("reconnect-token".into(), "user-device".into(), 0);
let connection_id = "conn-reconnect-cycle";
for cycle in 0..5 {
client.reject_session_connection(connection_id, "session-admission-timeout");
assert!(
matches!(
client.session_admission(connection_id),
crate::session_token::SessionAdmission::Rejected { .. }
),
"cycle {}: should be rejected",
cycle
);
client.forget_session_connection(connection_id);
assert!(
matches!(
client.session_admission(connection_id),
crate::session_token::SessionAdmission::Pending
),
"cycle {}: should be pending after reset",
cycle
);
let scope = client
.validate_session_token_for_connection("reconnect-token", connection_id)
.await
.expect(&format!("cycle {}: token should still validate", cycle));
assert_eq!(scope, "user-device");
}
}
#[tokio::test]
async fn session_token_presentation_message_roundtrips_correctly() {
let msg = NativeMainMessage::session_token_presentation("my-secret-token");
let serialized = serde_json::to_vec(&msg).expect("serialize");
let deserialized: NativeMainMessage =
serde_json::from_slice(&serialized).expect("deserialize");
assert!(
deserialized.is_session_token_presentation(),
"deserialized message should be a session-token presentation"
);
let extracted_token = deserialized
.payload
.get("token")
.and_then(|v| v.as_str())
.expect("token field");
assert_eq!(extracted_token, "my-secret-token");
}
#[tokio::test]
async fn auto_connect_tie_break_initiator_role_determined_by_node_id_comparison() {
let high_node = "zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz";
let low_node = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa";
let decision = auto_connect_tie_break_decision(
high_node, low_node, false, 0, 3_000, );
assert!(
matches!(decision, AutoConnectTieBreakDecision::ActAsInitiator),
"higher node_id should act as initiator"
);
let decision = auto_connect_tie_break_decision(low_node, high_node, false, 0, 3_000);
assert!(
matches!(decision, AutoConnectTieBreakDecision::WaitForInitiator),
"lower node_id should wait for initiator"
);
let decision = auto_connect_tie_break_decision(
low_node, high_node, false, 4_000, 3_000,
);
assert!(
matches!(decision, AutoConnectTieBreakDecision::ActAsInitiator),
"non-initiator should escalate after grace period"
);
}
#[tokio::test]
async fn full_auto_connect_sequence_native_initiator_to_wasm_peer() {
let host = Client::new_with_app_tag(
crate::test_constants::TEST_PROJECT_ID.to_string(),
"test-app".to_string(),
Box::new(|| None),
);
let token = "host-user-device-token-xyz";
host.register_session_token(token.into(), "user-device".into(), 0);
let iroh_ticket = "endpointABCDEF1234567890abcdef";
let compound =
crate::session_token::build_compound_ticket(iroh_ticket, token, "user-device", 0);
let (recovered_iroh, suffix) = crate::session_token::split_compound_ticket(&compound);
assert_eq!(recovered_iroh, iroh_ticket);
let extracted_token = suffix
.and_then(|value| {
crate::session_token::decode_token_payload_for_ticket(recovered_iroh, value)
})
.map(|p| p.token)
.expect("token should be extractable");
assert_eq!(extracted_token, token);
let wasm_node_id = "wasm-peer-node-id-1234";
let wasm_device_id = "wasm-peer-device-id-1234";
let connection_id = Client::deterministic_connection_id("host-node-id", wasm_node_id);
host.connection_manager
.upsert_pending(
connection_id.clone(),
Some(wasm_node_id.to_string()),
Some(wasm_device_id.to_string()),
Some(wasm_node_id.to_string()),
)
.await;
host.connection_manager
.set_connected_with_transport(
&connection_id,
Some(wasm_node_id.to_string()),
Some(77),
Some("managed".to_string()),
)
.await
.expect("set connected");
let scope = host
.validate_session_token_for_connection(&extracted_token, &connection_id)
.await
.expect("token presentation should succeed");
assert_eq!(scope, "user-device");
establish_current_managed_user_device_route(&host, &connection_id, 77);
let record = host
.connection_manager
.get_by_connection_id(&connection_id)
.await
.expect("record exists");
let adoption = host
.managed_connection_adoption(&record)
.await
.expect("adoption should succeed for admitted connection");
assert_eq!(adoption.connection_id, connection_id);
assert_eq!(adoption.node_id, wasm_node_id);
assert_eq!(adoption.device_id.as_deref(), Some(wasm_device_id));
assert!(adoption.main_stream_ready);
}
#[tokio::test]
async fn full_auto_connect_sequence_wasm_initiator_to_native_host() {
let host = Client::new_with_app_tag(
crate::test_constants::TEST_PROJECT_ID.to_string(),
"test-app".to_string(),
Box::new(|| None),
);
let token = "native-host-token-for-wasm";
host.register_session_token(token.into(), "user-device".into(), 0);
let native_node = "native-host-node-aaa";
let wasm_node = "wasm-client-node-zzz";
let connection_id = Client::deterministic_connection_id(native_node, wasm_node);
host.connection_manager
.upsert_pending(
connection_id.clone(),
Some(wasm_node.to_string()),
Some("wasm-device-id-123".to_string()),
Some(wasm_node.to_string()),
)
.await;
host.connection_manager
.set_connected_with_transport(
&connection_id,
Some(wasm_node.to_string()),
Some(88),
Some("incoming".to_string()),
)
.await
.expect("set connected");
host.connection_manager
.set_device_id(&connection_id, "wasm-device-id-123".to_string())
.await
.expect("set device_id");
let record = host
.connection_manager
.get_by_connection_id(&connection_id)
.await
.expect("record exists");
assert!(
host.managed_connection_adoption(&record).await.is_none(),
"adoption should be blocked before token admission"
);
let scope = host
.validate_session_token_for_connection(token, &connection_id)
.await
.expect("token should validate");
assert_eq!(scope, "user-device");
establish_current_managed_user_device_route(&host, &connection_id, 88);
let adoption = host
.managed_connection_adoption(&record)
.await
.expect("adoption should succeed after token admission");
assert_eq!(adoption.device_id.as_deref(), Some("wasm-device-id-123"));
assert!(adoption.main_stream_ready);
}
#[tokio::test]
async fn duplicate_resolution_with_token_admission_preserves_admitted_state() {
let host = Client::new_with_app_tag(
crate::test_constants::TEST_PROJECT_ID.to_string(),
"test-app".to_string(),
Box::new(|| None),
);
host.register_session_token("dup-token".into(), "user-device".into(), 0);
let connection_id = "conn-dup-test";
host.validate_session_token_for_connection("dup-token", connection_id)
.await
.expect("token validates");
assert!(matches!(
host.session_admission(connection_id),
crate::session_token::SessionAdmission::Accepted { .. }
));
assert!(matches!(
host.session_admission(connection_id),
crate::session_token::SessionAdmission::Accepted { .. }
));
}
#[tokio::test]
async fn forget_on_retirement_enables_reconnect_for_same_peer() {
let host = Client::new_with_app_tag(
crate::test_constants::TEST_PROJECT_ID.to_string(),
"test-app".to_string(),
Box::new(|| None),
);
host.register_session_token("retire-token".into(), "user-device".into(), 0);
let node_a = "node-local-host";
let node_b = "node-remote-peer";
let connection_id = Client::deterministic_connection_id(node_a, node_b);
host.validate_session_token_for_connection("retire-token", &connection_id)
.await
.expect("first admission");
assert!(matches!(
host.session_admission(&connection_id),
crate::session_token::SessionAdmission::Accepted { .. }
));
host.forget_session_connection(&connection_id);
assert!(matches!(
host.session_admission(&connection_id),
crate::session_token::SessionAdmission::Pending
));
let scope = host
.validate_session_token_for_connection("retire-token", &connection_id)
.await
.expect("second admission should succeed after retirement forget");
assert_eq!(scope, "user-device");
}
#[test]
fn ensure_default_admission_gate_activates_registry_before_any_connection() {
let client = Client::new_with_app_tag(
crate::test_constants::TEST_PROJECT_ID.to_string(),
"test-app".to_string(),
Box::new(|| None),
);
assert!(
!client.session_registry_active(),
"registry should be inactive before startup"
);
client.ensure_default_admission_gate("__startup");
assert!(
client.session_registry_active(),
"registry should be active after startup gate"
);
assert!(matches!(
client.session_admission("any-connection"),
crate::session_token::SessionAdmission::Pending
));
}
#[test]
fn protect_outbound_application_payload_fails_closed_when_required_key_missing() {
let client = Client::new_with_app_tag(
crate::test_constants::TEST_PROJECT_ID.to_string(),
"test-app".to_string(),
Box::new(|| None),
);
client.set_connection_application_crypto_required("conn-1");
let err = client
.protect_outbound_application_payload("conn-1", b"hello")
.expect_err("missing key should fail when crypto is required");
assert!(err.to_string().contains("application crypto required"));
}
#[test]
fn protect_outbound_application_payload_passthrough_when_not_required() {
let client = Client::new_with_app_tag(
crate::test_constants::TEST_PROJECT_ID.to_string(),
"test-app".to_string(),
Box::new(|| None),
);
let protected = client
.protect_outbound_application_payload("conn-1", b"hello")
.expect("plaintext passthrough allowed when crypto not required");
assert_eq!(protected, b"hello");
}
#[test]
fn reinstalling_identical_application_crypto_key_preserves_outbound_sequence() {
let client = Client::new_with_app_tag(
crate::test_constants::TEST_PROJECT_ID.to_string(),
"test-app".to_string(),
Box::new(|| None),
);
let connection_id = "conn-stable-key";
let key = [7u8; crate::application_crypto::APPLICATION_KEY_BYTES];
client.set_connection_application_crypto_key(connection_id, key);
client
.protect_outbound_application_payload(connection_id, b"first")
.expect("first payload should encrypt");
client.set_connection_application_crypto_key(connection_id, key);
let sequence = client
.connection_application_crypto_outbound_sequences
.read()
.expect("sequence map should be readable")
.get(connection_id)
.copied();
assert_eq!(sequence, Some(1));
client.set_connection_application_crypto_key(
connection_id,
[9u8; crate::application_crypto::APPLICATION_KEY_BYTES],
);
let reset_sequence = client
.connection_application_crypto_outbound_sequences
.read()
.expect("sequence map should be readable")
.get(connection_id)
.copied();
assert_eq!(reset_sequence, Some(0));
}
#[tokio::test]
async fn exact_connection_key_wins_over_competing_endpoint_generation() {
let client = Client::new_with_app_tag(
crate::test_constants::TEST_PROJECT_ID.to_string(),
"test-app".to_string(),
Box::new(|| None),
);
let endpoint_id: iroh::EndpointId =
"5d388fcaaa61efa536d78051e060928f6c1fff0983a32f6b532176b12328c3ac"
.parse()
.expect("endpoint id");
let endpoint = endpoint_id.to_string();
let active_connection_id = "conn-active-generation";
let competing_connection_id = "conn-competing-generation";
let active_key = [7u8; crate::application_crypto::APPLICATION_KEY_BYTES];
let competing_key = [9u8; crate::application_crypto::APPLICATION_KEY_BYTES];
client
.connection_manager
.upsert_pending(
active_connection_id.to_string(),
Some(endpoint.clone()),
Some("device-active-generation".to_string()),
Some(endpoint.clone()),
)
.await;
client
.connection_manager
.upsert_pending(
competing_connection_id.to_string(),
Some(endpoint.clone()),
Some("device-competing-generation".to_string()),
Some(endpoint.clone()),
)
.await;
client
.connection_manager
.set_connected_with_transport(
competing_connection_id,
Some(endpoint),
Some(99),
Some("replacement-generation".to_string()),
)
.await
.expect("competing endpoint record");
client.set_connection_application_crypto_key(active_connection_id, active_key);
client.set_connection_application_crypto_key(competing_connection_id, competing_key);
assert_eq!(
client
.application_crypto_key_for_connection_or_endpoint(
Some(active_connection_id),
&endpoint_id,
)
.await,
Some(active_key),
);
}
#[tokio::test]
async fn stale_projected_connection_key_cannot_cross_endpoint_generation() {
let client = Client::new_with_app_tag(
crate::test_constants::TEST_PROJECT_ID.to_string(),
"test-app".to_string(),
Box::new(|| None),
);
let current_endpoint: iroh::EndpointId =
"5d388fcaaa61efa536d78051e060928f6c1fff0983a32f6b532176b12328c3ac"
.parse()
.expect("current endpoint id");
let stale_endpoint: iroh::EndpointId =
"4af216294322ea5ee43b4d7faccd50c6c89da6f9b3383ee1a9e07bfcb1a860ed"
.parse()
.expect("stale endpoint id");
let stale_connection_id = "conn-stale-browser-generation";
let current_connection_id = "conn-current-browser-generation";
let stale_key = [7u8; crate::application_crypto::APPLICATION_KEY_BYTES];
let current_key = [9u8; crate::application_crypto::APPLICATION_KEY_BYTES];
for (connection_id, endpoint) in [
(stale_connection_id, stale_endpoint.to_string()),
(current_connection_id, current_endpoint.to_string()),
] {
client
.connection_manager
.upsert_pending(
connection_id.to_string(),
Some(endpoint.clone()),
Some("durable-browser-device".to_string()),
Some(endpoint.clone()),
)
.await;
client
.connection_manager
.set_connected(connection_id, Some(endpoint))
.await;
}
client.set_connection_application_crypto_key(stale_connection_id, stale_key);
client.set_connection_application_crypto_key(current_connection_id, current_key);
assert_eq!(
client
.application_crypto_key_for_connection_or_endpoint(
Some(stale_connection_id),
¤t_endpoint,
)
.await,
Some(current_key),
);
}
#[cfg(all(
not(target_arch = "wasm32"),
any(feature = "transport-webrtc", feature = "transport-moq")
))]
#[tokio::test]
async fn native_transport_peer_data_event_opens_application_crypto_payload() {
let client = Client::new_with_app_tag(
crate::test_constants::TEST_PROJECT_ID.to_string(),
"test-app".to_string(),
Box::new(|| None),
);
let mut rx = client.subscribe_native_peer_data();
let key = [7u8; crate::application_crypto::APPLICATION_KEY_BYTES];
let nonce = [3u8; crate::application_crypto::APPLICATION_NONCE_BYTES];
client.set_connection_application_crypto_key("conn-1", key);
let protected = crate::application_crypto::protect_application_payload_with_nonce(
&key,
0,
b"hello over moq",
&nonce,
)
.expect("payload should encrypt");
client
.handle_native_transport_peer_data(
"conn-1",
Some("remote-node"),
"moq",
crate::client::NativePeerDataGeneration {
transport_stable_id: 31,
transport_generation: 2,
route_generation: 4,
},
bytes::Bytes::from(protected),
)
.await
.expect("transport payload should open and emit");
let event = tokio::time::timeout(Duration::from_secs(1), rx.recv())
.await
.expect("native peer data event should arrive")
.expect("native peer data channel should stay open");
assert_eq!(event.connection_id, "conn-1");
assert_eq!(event.remote_node_id.as_deref(), Some("remote-node"));
assert_eq!(event.transport, "moq");
assert_eq!(event.transport_stable_id, 31);
assert_eq!(event.transport_generation, 2);
assert_eq!(event.route_generation, 4);
assert_eq!(event.payload, b"hello over moq");
}
#[cfg(all(
not(target_arch = "wasm32"),
any(feature = "transport-webrtc", feature = "transport-moq")
))]
#[tokio::test]
async fn native_moq_peer_data_event_opens_raw_stream_application_crypto_payload() {
let client = Client::new_with_app_tag(
crate::test_constants::TEST_PROJECT_ID.to_string(),
"test-app".to_string(),
Box::new(|| None),
);
let mut rx = client.subscribe_native_peer_data();
let key = [9u8; crate::application_crypto::APPLICATION_KEY_BYTES];
let nonce = [4u8; crate::application_crypto::APPLICATION_NONCE_BYTES];
client.set_connection_application_crypto_key("conn-1", key);
let protected = crate::application_crypto::protect_application_payload_with_nonce(
&key,
crate::application_crypto::RAW_STREAM_TYPE_ID,
b"hello over direct moq",
&nonce,
)
.expect("payload should encrypt");
client
.handle_native_transport_peer_data(
"conn-1",
Some("remote-node"),
"moq",
crate::client::NativePeerDataGeneration {
transport_stable_id: 32,
transport_generation: 3,
route_generation: 5,
},
bytes::Bytes::from(protected),
)
.await
.expect("direct moq payload should open and emit");
let event = tokio::time::timeout(Duration::from_secs(1), rx.recv())
.await
.expect("native peer data event should arrive")
.expect("native peer data channel should stay open");
assert_eq!(event.connection_id, "conn-1");
assert_eq!(event.remote_node_id.as_deref(), Some("remote-node"));
assert_eq!(event.transport, "moq");
assert_eq!(event.transport_stable_id, 32);
assert_eq!(event.transport_generation, 3);
assert_eq!(event.route_generation, 5);
assert_eq!(event.payload, b"hello over direct moq");
}
#[cfg(all(not(target_arch = "wasm32"), feature = "transport-moq"))]
#[test]
fn native_moq_route_proof_requires_crypto_and_the_raw_stream_envelope() {
let client = Client::new_with_app_tag(
crate::test_constants::TEST_PROJECT_ID.to_string(),
"test-app".to_string(),
Box::new(|| None),
);
let connection_id = "conn-moq-route-proof";
let key = [13u8; crate::application_crypto::APPLICATION_KEY_BYTES];
let plaintext = b"{\"type\":\"#openrtc-moq-route-proof\"}";
assert!(client
.protect_outbound_direct_moq_payload(connection_id, plaintext)
.is_err());
assert!(client
.open_inbound_direct_moq_route_proof(connection_id, plaintext)
.is_err());
client.set_connection_application_crypto_key(connection_id, key);
assert!(client
.open_inbound_direct_moq_route_proof(connection_id, plaintext)
.is_err());
let type_zero = crate::application_crypto::protect_application_payload_with_nonce(
&key,
0,
plaintext,
&[5u8; crate::application_crypto::APPLICATION_NONCE_BYTES],
)
.expect("type-zero payload should encrypt");
assert!(client
.open_inbound_direct_moq_route_proof(connection_id, &type_zero)
.is_err());
assert_eq!(
client
.open_inbound_direct_moq_payload(connection_id, &type_zero)
.expect("existing MoQ product payload behavior should remain compatible"),
plaintext,
);
let type_three = crate::application_crypto::protect_application_payload_with_nonce(
&key,
crate::application_crypto::RAW_STREAM_TYPE_ID,
plaintext,
&[6u8; crate::application_crypto::APPLICATION_NONCE_BYTES],
)
.expect("raw-stream payload should encrypt");
assert_eq!(
client
.open_inbound_direct_moq_route_proof(connection_id, &type_three)
.expect("type-three MoQ proof should open"),
plaintext,
);
}
#[cfg(all(not(target_arch = "wasm32"), feature = "transport-moq"))]
#[tokio::test(start_paused = true)]
async fn native_moq_explicit_send_does_not_retain_a_failed_repair_candidate() {
let client = Client::new_with_app_tag(
crate::test_constants::TEST_PROJECT_ID.to_string(),
"test-app".to_string(),
Box::new(|| None),
);
*client.node_id.write().await = Some("local-node".to_string());
client
.update_transport_config(TransportConfig {
moq: Some(MoQConfig {
relay_url: "https://relay.invalid/moq".to_string(),
..MoQConfig::default()
}),
..TransportConfig::default()
})
.await
.expect("transport config");
client
.connection_manager
.upsert_pending(
"conn-moq".to_string(),
Some("remote-node".to_string()),
Some("remote-device".to_string()),
Some("remote-node".to_string()),
)
.await;
client
.connection_manager
.set_connected_with_transport(
"conn-moq",
Some("remote-node".to_string()),
Some(1),
Some("incoming".to_string()),
)
.await
.expect("connected record");
client.set_connection_application_crypto_key(
"conn-moq",
[11u8; crate::application_crypto::APPLICATION_KEY_BYTES],
);
assert_eq!(
client.resolve_peer_connection_ids("remote-device").await,
vec!["conn-moq".to_string()],
"device id should resolve to the managed connection before MoQ repair",
);
assert_eq!(
client
.resolve_transport_connection_ids("remote-device")
.await,
vec!["remote-device".to_string(), "conn-moq".to_string()],
"transport lookup checks the raw transport key first, then the managed connection",
);
let error = client
.send_peer_over_moq("remote-device", b"hello over moq")
.await
.expect_err("invalid relay should still fail the final MoQ-only proof");
assert!(
error
.to_string()
.contains("MoQ transport is not data-ready"),
"unexpected error: {error}",
);
assert!(error.to_string().contains("no native MoQ session"));
let session_keys = client
.native_moq_sessions
.read()
.await
.keys()
.cloned()
.collect::<Vec<_>>();
assert_eq!(
session_keys,
Vec::<String>::new(),
"a terminal setup failure must not leave a stale MoQ session owner",
);
assert!(client
.native_moq_state_for_peer("remote-device")
.await
.is_none());
}
#[cfg(all(not(target_arch = "wasm32"), feature = "transport-moq"))]
#[tokio::test]
async fn native_moq_failed_setup_is_retired_before_the_next_repair() {
let client = Client::new_with_app_tag(
crate::test_constants::TEST_PROJECT_ID.to_string(),
"test-app".to_string(),
Box::new(|| None),
);
*client.node_id.write().await = Some("local-node".to_string());
client
.update_transport_config(TransportConfig {
moq: Some(MoQConfig {
relay_url: "https://relay.invalid/moq".to_string(),
..MoQConfig::default()
}),
..TransportConfig::default()
})
.await
.expect("transport config");
client
.connection_manager
.upsert_pending(
"conn-moq-repair".to_string(),
Some("remote-node-repair".to_string()),
Some("remote-device-repair".to_string()),
Some("remote-node-repair".to_string()),
)
.await;
client
.connection_manager
.set_connected_with_transport(
"conn-moq-repair",
Some("remote-node-repair".to_string()),
Some(1),
Some("incoming".to_string()),
)
.await
.expect("connected record");
assert!(client
.request_moq_upgrade(
"remote-device-repair",
Some("remote-node-repair"),
Some("test-initial")
)
.await
.expect("initial request"));
assert!(
client.native_moq_sessions.read().await.is_empty(),
"a failed setup must release its route owner before recovery",
);
assert!(client
.request_moq_upgrade(
"remote-device-repair",
Some("remote-node-repair"),
Some("test-repair")
)
.await
.expect("repair request"));
assert!(
client.native_moq_sessions.read().await.is_empty(),
"a repeated failed repair must not restore the retired route owner",
);
}
#[cfg(all(not(target_arch = "wasm32"), feature = "transport-moq"))]
#[tokio::test]
async fn native_moq_repair_retires_connected_session_without_peer_data_subscription() {
let client = Client::new_with_app_tag(
crate::test_constants::TEST_PROJECT_ID.to_string(),
"test-app".to_string(),
Box::new(|| None),
);
*client.node_id.write().await = Some("local-node".to_string());
let moq_config = MoQConfig {
relay_url: "https://relay.invalid/moq".to_string(),
..MoQConfig::default()
};
client
.update_transport_config(TransportConfig {
moq: Some(moq_config.clone()),
..TransportConfig::default()
})
.await
.expect("transport config");
client
.connection_manager
.upsert_pending(
"conn-moq-repair-not-ready".to_string(),
Some("remote-node-repair-not-ready".to_string()),
Some("remote-device-repair-not-ready".to_string()),
Some("remote-node-repair-not-ready".to_string()),
)
.await;
client
.connection_manager
.set_connected_with_transport(
"conn-moq-repair-not-ready",
Some("remote-node-repair-not-ready".to_string()),
Some(1),
Some("incoming".to_string()),
)
.await
.expect("connected record");
let first = std::sync::Arc::new(
crate::transport::NativeMoQSession::new(
"local-node",
"remote-node-repair-not-ready",
&moq_config,
)
.await
.expect("test session"),
);
first.force_state_for_test(crate::transport::NativeMoQState::Connected);
client
.native_moq_sessions
.write()
.await
.insert("conn-moq-repair-not-ready".to_string(), first.clone());
client
.install_native_optional_route_generation(
"conn-moq-repair-not-ready",
crate::route_policy::KnownRoute::Moq,
std::sync::Arc::as_ptr(&first) as usize,
)
.await
.expect("current MoQ route generation");
assert!(!first.is_peer_data_subscribed().await);
let (connection_id, state, data_ready) = client
.native_moq_state_detail_for_peer("remote-device-repair-not-ready")
.await
.expect("native MoQ state detail");
assert_eq!(connection_id, "conn-moq-repair-not-ready");
assert_eq!(state, crate::transport::NativeMoQState::Connected);
assert!(
!data_ready,
"connected native MoQ sessions are not ready until peer data subscription is active"
);
assert!(client
.request_moq_repair(
"remote-device-repair-not-ready",
Some("remote-node-repair-not-ready"),
Some("test-repair-not-ready")
)
.await
.expect("repair request"));
assert!(
client.native_moq_sessions.read().await.is_empty(),
"a failed replacement setup must not retain a non-routable MoQ owner",
);
assert_eq!(first.state(), crate::transport::NativeMoQState::Closed);
}
#[cfg(not(target_arch = "wasm32"))]
#[test]
fn inbound_session_admission_is_scoped_to_physical_transport_generation() {
let client = Client::new_with_app_tag(
crate::test_constants::TEST_PROJECT_ID.to_string(),
"test-app".to_string(),
Box::new(|| None),
);
let connection_id = "logical-session-1";
assert!(!client.inbound_session_token_admitted_for_transport(connection_id, 10));
client.bind_inbound_session_token_transport(connection_id, 10);
assert!(client.inbound_session_token_admitted_for_transport(connection_id, 10));
assert!(!client.inbound_session_token_admitted_for_transport(connection_id, 11));
client.forget_session_connection(connection_id);
assert!(!client.inbound_session_token_admitted_for_transport(connection_id, 10));
}
#[cfg(not(target_arch = "wasm32"))]
#[test]
fn authenticated_replacement_invalidates_only_stale_route_proofs() {
let client = Client::new_with_app_tag(
crate::test_constants::TEST_PROJECT_ID.to_string(),
"test-app".to_string(),
Box::new(|| None),
);
let connection_id = "logical-session-rebind";
client.session_token_registry.mark_accepted(
connection_id,
crate::session_token::SessionAdmissionMechanism::SessionToken,
Some(crate::session_token::GrantScope::from("user-device")),
Some("device-remote".to_string()),
);
client.bind_inbound_native_admission_stream_contract(
connection_id,
crate::native_protocol::SessionTokenStreamContract::PersistentControl,
);
client.bind_outbound_native_admission_stream_contract(
connection_id,
crate::native_protocol::SessionTokenStreamContract::PersistentControl,
);
client.bind_inbound_session_token_transport(connection_id, 10);
client
.remote_session_admission_proofs
.write()
.expect("remote proof registry")
.insert(
connection_id.to_string(),
RemoteSessionAdmissionProof {
transport_stable_id: 10,
token_fingerprint: "token-fingerprint".to_string(),
approval_scope: "user-device".to_string(),
},
);
assert!(client.invalidate_native_main_route_proofs_except_transport(connection_id, 11));
assert!(!client.inbound_session_token_admitted_for_transport(connection_id, 10));
assert!(!client.native_declared_persistent_routes_are_proven(connection_id));
assert!(matches!(
client.session_admission(connection_id),
crate::session_token::SessionAdmission::Accepted { .. }
));
}
#[cfg(not(target_arch = "wasm32"))]
#[test]
fn authenticated_replacement_requires_fresh_bilateral_route_proofs() {
let client = Client::new_with_app_tag(
crate::test_constants::TEST_PROJECT_ID.to_string(),
"test-app".to_string(),
Box::new(|| None),
);
let connection_id = "logical-session-authenticated-replacement";
client.register_session_token(
"replacement-token".to_string(),
"user-device".to_string(),
1,
);
client.session_token_registry.mark_accepted(
connection_id,
crate::session_token::SessionAdmissionMechanism::SessionToken,
Some(crate::session_token::GrantScope::from("user-device")),
Some("device-remote".to_string()),
);
client.bind_inbound_native_admission_stream_contract(
connection_id,
crate::native_protocol::SessionTokenStreamContract::PersistentControl,
);
client.bind_outbound_native_admission_stream_contract(
connection_id,
crate::native_protocol::SessionTokenStreamContract::PersistentControl,
);
client.bind_inbound_session_token_transport(connection_id, 10);
client
.remote_session_admission_proofs
.write()
.expect("remote proof registry")
.insert(
connection_id.to_string(),
RemoteSessionAdmissionProof {
transport_stable_id: 10,
token_fingerprint: "token-fingerprint".to_string(),
approval_scope: "user-device".to_string(),
},
);
assert!(client.invalidate_native_main_route_proofs_except_transport(connection_id, 11));
assert!(!client.inbound_session_token_admitted_for_transport(connection_id, 11));
assert_eq!(
client.session_admission_block_reason_for_transport(connection_id, Some(11)),
Some((false, "native-main-route-pending".to_string())),
"the replacement must not inherit either directional route proof",
);
client.bind_inbound_session_token_transport(connection_id, 11);
assert_eq!(
client.session_admission_block_reason_for_transport(connection_id, Some(11)),
Some((false, "native-main-route-pending".to_string())),
"one fresh direction cannot make a bilateral user-device route ready",
);
client
.remote_session_admission_proofs
.write()
.expect("remote proof registry")
.insert(
connection_id.to_string(),
RemoteSessionAdmissionProof {
transport_stable_id: 11,
token_fingerprint: "replacement-token-fingerprint".to_string(),
approval_scope: "user-device".to_string(),
},
);
assert_eq!(
client.session_admission_block_reason_for_transport(connection_id, Some(11)),
None,
);
}
#[cfg(not(target_arch = "wasm32"))]
#[tokio::test]
async fn incoming_replacement_retires_previous_generation_route_proofs() {
let client = Client::new_with_app_tag(
crate::test_constants::TEST_PROJECT_ID.to_string(),
"test-app".to_string(),
Box::new(|| None),
);
let connection_id = "logical-session-incoming-replacement";
client.register_session_token(
"incoming-replacement-token".to_string(),
"user-device".to_string(),
1,
);
client.session_token_registry.mark_accepted(
connection_id,
crate::session_token::SessionAdmissionMechanism::SessionToken,
Some(crate::session_token::GrantScope::from("user-device")),
Some("device-remote".to_string()),
);
client.bind_inbound_native_admission_stream_contract(
connection_id,
crate::native_protocol::SessionTokenStreamContract::PersistentControl,
);
client.bind_outbound_native_admission_stream_contract(
connection_id,
crate::native_protocol::SessionTokenStreamContract::PersistentControl,
);
client.bind_inbound_session_token_transport(connection_id, 10);
client
.remote_session_admission_proofs
.write()
.expect("remote proof registry")
.insert(
connection_id.to_string(),
RemoteSessionAdmissionProof {
transport_stable_id: 10,
token_fingerprint: "old-token-fingerprint".to_string(),
approval_scope: "user-device".to_string(),
},
);
let previous_application_key = [41u8; crate::application_crypto::APPLICATION_KEY_BYTES];
client.set_connection_application_crypto_key(connection_id, previous_application_key);
client.set_connection_application_crypto_required(connection_id);
let _ = client.get_or_create_connection_key_agreement(connection_id);
client.confirm_connection_application_crypto(connection_id);
assert!(client.connection_application_crypto_is_confirmed(connection_id));
assert!(
client
.retire_stale_native_main_route(connection_id, 11)
.await
);
assert!(!client.inbound_session_token_admitted_for_transport(connection_id, 10));
assert_eq!(
client.session_admission_block_reason_for_transport(connection_id, Some(11)),
Some((false, "native-main-route-pending".to_string())),
);
assert_eq!(
client.connection_application_crypto_key(connection_id),
Some(previous_application_key),
"transport replacement may preserve the negotiated key for an in-process migration",
);
assert!(
!client.connection_application_crypto_is_confirmed(connection_id),
"replacement product traffic must wait for fresh bilateral key confirmation",
);
assert!(matches!(
client.session_admission(connection_id),
crate::session_token::SessionAdmission::Accepted { .. }
));
}
#[cfg(not(target_arch = "wasm32"))]
#[tokio::test]
async fn current_transport_commit_rebinds_manager_and_fences_old_route_proofs() {
let client = Client::new_with_app_tag(
crate::test_constants::TEST_PROJECT_ID.to_string(),
"test-app".to_string(),
Box::new(|| None),
);
let connection_id = "logical-session-arbitration-commit";
let remote_node_id = "remote-node-arbitration-commit";
client.register_session_token(
"arbitration-token".to_string(),
"user-device".to_string(),
1,
);
client.session_token_registry.mark_accepted(
connection_id,
crate::session_token::SessionAdmissionMechanism::SessionToken,
Some(crate::session_token::GrantScope::from("user-device")),
Some("device-remote".to_string()),
);
client
.connection_manager
.upsert_pending(
connection_id.to_string(),
Some(remote_node_id.to_string()),
Some("device-remote".to_string()),
Some(remote_node_id.to_string()),
)
.await;
client
.connection_manager
.set_connected_with_transport(
connection_id,
Some(remote_node_id.to_string()),
Some(10),
Some("incoming-stale-handler".to_string()),
)
.await;
client.bind_inbound_native_admission_stream_contract(
connection_id,
crate::native_protocol::SessionTokenStreamContract::PersistentControl,
);
client.bind_outbound_native_admission_stream_contract(
connection_id,
crate::native_protocol::SessionTokenStreamContract::PersistentControl,
);
client.bind_inbound_session_token_transport(connection_id, 10);
client
.remote_session_admission_proofs
.write()
.expect("remote proof registry")
.insert(
connection_id.to_string(),
RemoteSessionAdmissionProof {
transport_stable_id: 10,
token_fingerprint: "old-token-fingerprint".to_string(),
approval_scope: "user-device".to_string(),
},
);
client
.commit_current_native_transport_record(
connection_id,
remote_node_id,
Some("device-remote".to_string()),
11,
Some("physical-arbitration-reconcile".to_string()),
)
.await;
let record = client
.connection_manager
.get_by_connection_id(connection_id)
.await
.expect("rebound connection record");
assert_eq!(record.transport_stable_id, Some(11));
assert_eq!(record.transport_generation, 2);
assert!(!client.inbound_session_token_admitted_for_transport(connection_id, 10));
assert!(!client.remote_session_token_admitted_for_transport(connection_id, 10));
assert_eq!(
client.session_admission_block_reason_for_transport(connection_id, Some(11)),
Some((false, "native-main-route-pending".to_string())),
"the low-level winner must be committed without inheriting stale route proof",
);
}
#[cfg(not(target_arch = "wasm32"))]
#[test]
fn transient_replacement_requires_fresh_remote_host_approval() {
let client = Client::new_with_app_tag(
crate::test_constants::TEST_PROJECT_ID.to_string(),
"test-app".to_string(),
Box::new(|| None),
);
let connection_id = "logical-transient-replacement";
client.session_token_registry.mark_accepted(
connection_id,
crate::session_token::SessionAdmissionMechanism::SessionToken,
Some(crate::session_token::GrantScope::from("share:revocable")),
None,
);
client.bind_outbound_native_admission_stream_contract(
connection_id,
crate::native_protocol::SessionTokenStreamContract::OneShotAdmission,
);
client
.remote_session_admission_proofs
.write()
.expect("remote proof registry")
.insert(
connection_id.to_string(),
RemoteSessionAdmissionProof {
transport_stable_id: 10,
token_fingerprint: "transient-token".to_string(),
approval_scope: "share:revocable".to_string(),
},
);
assert!(client.native_application_stream_admitted_for_transport(connection_id, 10));
assert!(client.invalidate_native_main_route_proofs_except_transport(connection_id, 11));
assert!(
!client.native_application_stream_admitted_for_transport(connection_id, 11),
"the replacement must remain closed until a fresh host approval is recorded",
);
}
#[cfg(not(target_arch = "wasm32"))]
#[test]
fn current_transport_route_proofs_are_not_invalidated() {
let client = Client::new_with_app_tag(
crate::test_constants::TEST_PROJECT_ID.to_string(),
"test-app".to_string(),
Box::new(|| None),
);
let connection_id = "logical-session-current-route";
client.bind_inbound_native_admission_stream_contract(
connection_id,
crate::native_protocol::SessionTokenStreamContract::PersistentControl,
);
client.bind_outbound_native_admission_stream_contract(
connection_id,
crate::native_protocol::SessionTokenStreamContract::PersistentControl,
);
client.bind_inbound_session_token_transport(connection_id, 10);
client
.remote_session_admission_proofs
.write()
.expect("remote proof registry")
.insert(
connection_id.to_string(),
RemoteSessionAdmissionProof {
transport_stable_id: 10,
token_fingerprint: "token-fingerprint".to_string(),
approval_scope: "user-device".to_string(),
},
);
assert!(!client.invalidate_native_main_route_proofs_except_transport(connection_id, 10));
assert!(client.inbound_session_token_admitted_for_transport(connection_id, 10));
assert!(client.native_declared_persistent_routes_are_proven(connection_id));
}
#[cfg(not(target_arch = "wasm32"))]
#[test]
fn accepted_user_device_session_requires_bilateral_generation_proofs() {
let client = Client::new_with_app_tag(
crate::test_constants::TEST_PROJECT_ID.to_string(),
"test-app".to_string(),
Box::new(|| None),
);
let connection_id = "logical-session-route-readiness";
client.register_session_token("route-token".to_string(), "user-device".to_string(), 1);
client.session_token_registry.mark_accepted(
connection_id,
crate::session_token::SessionAdmissionMechanism::SessionToken,
Some(crate::session_token::GrantScope::from("user-device")),
Some("device-remote".to_string()),
);
client.bind_inbound_native_admission_stream_contract(
connection_id,
crate::native_protocol::SessionTokenStreamContract::PersistentControl,
);
client.bind_outbound_native_admission_stream_contract(
connection_id,
crate::native_protocol::SessionTokenStreamContract::PersistentControl,
);
client.bind_inbound_session_token_transport(connection_id, 10);
assert_eq!(
client.session_admission_block_reason(connection_id),
Some((false, "native-main-route-pending".to_string())),
);
client.invalidate_native_main_route_proofs_for_transport(connection_id, 10);
assert_eq!(
client.session_admission_block_reason(connection_id),
Some((false, "native-main-route-pending".to_string())),
);
client
.remote_session_admission_proofs
.write()
.expect("remote proof registry")
.insert(
connection_id.to_string(),
RemoteSessionAdmissionProof {
transport_stable_id: 10,
token_fingerprint: "token-fingerprint".to_string(),
approval_scope: "user-device".to_string(),
},
);
assert_eq!(
client.session_admission_block_reason(connection_id),
Some((false, "native-main-route-pending".to_string())),
);
client.bind_inbound_session_token_transport(connection_id, 11);
assert_eq!(
client.session_admission_block_reason(connection_id),
Some((false, "native-main-route-pending".to_string())),
"proofs from different physical generations must never report a routable route",
);
client.bind_inbound_session_token_transport(connection_id, 10);
assert_eq!(client.session_admission_block_reason(connection_id), None);
assert_eq!(
client.session_admission_block_reason_for_transport(connection_id, Some(11)),
Some((false, "native-main-route-pending".to_string())),
"public readiness must reject bilateral proofs retained from an older transport",
);
assert_eq!(
client.session_admission_block_reason_for_transport(connection_id, Some(10)),
None,
);
}
#[cfg(not(target_arch = "wasm32"))]
#[test]
fn accepted_user_device_one_shot_inbound_requires_only_current_receiver_proof() {
let client = Client::new_with_app_tag(
crate::test_constants::TEST_PROJECT_ID.to_string(),
"test-app".to_string(),
Box::new(|| None),
);
let connection_id = "logical-session-one-shot-user-device";
client.register_session_token("route-token".to_string(), "user-device".to_string(), 1);
client.session_token_registry.mark_accepted(
connection_id,
crate::session_token::SessionAdmissionMechanism::SessionToken,
Some(crate::session_token::GrantScope::from("user-device")),
Some("device-remote".to_string()),
);
client.bind_inbound_native_admission_stream_contract(
connection_id,
crate::native_protocol::SessionTokenStreamContract::OneShotAdmission,
);
client.bind_inbound_session_token_transport(connection_id, 10);
assert_eq!(client.session_admission_block_reason(connection_id), None);
assert_eq!(
client.session_admission_block_reason_for_transport(connection_id, Some(10)),
None,
);
assert_eq!(
client.session_admission_block_reason_for_transport(connection_id, Some(11)),
Some((false, "native-main-route-pending".to_string())),
"one-shot admission must remain fenced to the physical generation that received it",
);
assert!(client.native_application_stream_admitted_for_transport(connection_id, 10));
assert!(!client.native_application_stream_admitted_for_transport(connection_id, 11));
}
#[cfg(not(target_arch = "wasm32"))]
#[test]
fn outbound_persistent_control_uses_its_current_remote_admission_proof() {
let client = Client::new_with_app_tag(
crate::test_constants::TEST_PROJECT_ID.to_string(),
"test-app".to_string(),
Box::new(|| None),
);
let connection_id = "logical-session-outbound-persistent-only";
client.register_session_token("route-token".to_string(), "share".to_string(), 1);
client.session_token_registry.mark_accepted(
connection_id,
crate::session_token::SessionAdmissionMechanism::SessionToken,
Some(crate::session_token::GrantScope::from("share")),
None,
);
client.bind_outbound_native_admission_stream_contract(
connection_id,
crate::native_protocol::SessionTokenStreamContract::PersistentControl,
);
client
.remote_session_admission_proofs
.write()
.expect("remote proof registry")
.insert(
connection_id.to_string(),
RemoteSessionAdmissionProof {
transport_stable_id: 10,
token_fingerprint: "token-fingerprint".to_string(),
approval_scope: "share".to_string(),
},
);
assert_eq!(client.session_admission_block_reason(connection_id), None);
assert!(client.native_application_stream_admitted_for_transport(connection_id, 10));
assert!(
!client.native_application_stream_admitted_for_transport(connection_id, 11),
"a remote approval for an older physical generation must not admit product traffic",
);
}
#[cfg(not(target_arch = "wasm32"))]
#[test]
fn inbound_persistent_control_uses_its_current_transport_proof() {
let client = Client::new_with_app_tag(
crate::test_constants::TEST_PROJECT_ID.to_string(),
"test-app".to_string(),
Box::new(|| None),
);
let connection_id = "logical-session-inbound-persistent-only";
client.register_session_token("route-token".to_string(), "share".to_string(), 1);
client.session_token_registry.mark_accepted(
connection_id,
crate::session_token::SessionAdmissionMechanism::SessionToken,
Some(crate::session_token::GrantScope::from("share")),
None,
);
client.bind_inbound_native_admission_stream_contract(
connection_id,
crate::native_protocol::SessionTokenStreamContract::PersistentControl,
);
client.bind_inbound_session_token_transport(connection_id, 10);
assert_eq!(client.session_admission_block_reason(connection_id), None);
assert!(client.native_application_stream_admitted_for_transport(connection_id, 10));
assert!(
!client.native_application_stream_admitted_for_transport(connection_id, 11),
"an inbound presentation for an older physical generation must not admit product traffic",
);
}
#[cfg(not(target_arch = "wasm32"))]
#[test]
fn accepted_one_shot_session_requires_only_current_inbound_proof() {
let client = Client::new_with_app_tag(
crate::test_constants::TEST_PROJECT_ID.to_string(),
"test-app".to_string(),
Box::new(|| None),
);
let connection_id = "logical-session-one-shot-readiness";
client.register_session_token("route-token".to_string(), "share".to_string(), 1);
client.session_token_registry.mark_accepted(
connection_id,
crate::session_token::SessionAdmissionMechanism::SessionToken,
Some(crate::session_token::GrantScope::from("share")),
None,
);
client.bind_inbound_native_admission_stream_contract(
connection_id,
crate::native_protocol::SessionTokenStreamContract::OneShotAdmission,
);
assert_eq!(
client.session_admission_block_reason(connection_id),
Some((false, "native-main-route-pending".to_string())),
);
client.bind_inbound_session_token_transport(connection_id, 10);
assert_eq!(client.session_admission_block_reason(connection_id), None);
assert!(
!client.native_declared_persistent_routes_are_proven(connection_id),
"one-shot browser admission must not invent a reciprocal proof",
);
client.forget_session_connection(connection_id);
assert_eq!(
client.native_admission_stream_contracts(connection_id),
crate::client::NativeAdmissionStreamContracts::default(),
);
assert!(!client.inbound_session_token_admitted_for_transport(connection_id, 10));
}
#[cfg(not(target_arch = "wasm32"))]
#[test]
fn accepted_one_shot_session_keeps_product_streams_blocked_until_crypto_is_reciprocal() {
let client = Client::new_with_app_tag(
crate::test_constants::TEST_PROJECT_ID.to_string(),
"test-app".to_string(),
Box::new(|| None),
);
let connection_id = "logical-session-one-shot-crypto";
client.register_session_token("route-token".to_string(), "share".to_string(), 1);
client.session_token_registry.mark_accepted(
connection_id,
crate::session_token::SessionAdmissionMechanism::SessionToken,
Some(crate::session_token::GrantScope::from("share")),
None,
);
client.bind_inbound_native_admission_stream_contract(
connection_id,
crate::native_protocol::SessionTokenStreamContract::OneShotAdmission,
);
client.bind_inbound_session_token_transport(connection_id, 10);
client.set_connection_application_crypto_required(connection_id);
assert!(
!client.native_application_stream_admitted_for_transport(connection_id, 10),
"token admission must not expose product streams before a key is installed",
);
let _agreement = client
.get_or_create_connection_key_agreement(connection_id)
.expect("key agreement");
client.set_connection_application_crypto_key(connection_id, [7u8; 32]);
assert!(
!client.native_application_stream_admitted_for_transport(connection_id, 10),
"one-sided key derivation must not expose product streams",
);
client.confirm_connection_application_crypto(connection_id);
assert!(client.native_application_stream_admitted_for_transport(connection_id, 10));
}
#[cfg(not(target_arch = "wasm32"))]
#[test]
fn native_browser_admission_contracts_remain_directional() {
let client = Client::new_with_app_tag(
crate::test_constants::TEST_PROJECT_ID.to_string(),
"test-app".to_string(),
Box::new(|| None),
);
let connection_id = "logical-session-mixed-direction-contract";
client.bind_inbound_native_admission_stream_contract(
connection_id,
crate::native_protocol::SessionTokenStreamContract::OneShotAdmission,
);
client.bind_outbound_native_admission_stream_contract(
connection_id,
crate::native_protocol::SessionTokenStreamContract::PersistentControl,
);
assert_eq!(
client.native_admission_stream_contracts(connection_id),
crate::client::NativeAdmissionStreamContracts {
inbound: Some(crate::native_protocol::SessionTokenStreamContract::OneShotAdmission,),
outbound: Some(
crate::native_protocol::SessionTokenStreamContract::PersistentControl,
),
},
);
assert!(client.native_admission_stream_is_persistent(connection_id));
}
#[cfg(not(target_arch = "wasm32"))]
#[test]
fn persistent_control_cleanup_preserves_one_shot_inbound_proof() {
let client = Client::new_with_app_tag(
crate::test_constants::TEST_PROJECT_ID.to_string(),
"test-app".to_string(),
Box::new(|| None),
);
let connection_id = "logical-session-mixed-proof-lifetimes";
client.register_session_token("route-token".to_string(), "user-device".to_string(), 1);
client.session_token_registry.mark_accepted(
connection_id,
crate::session_token::SessionAdmissionMechanism::SessionToken,
Some(crate::session_token::GrantScope::from("user-device")),
Some("device-remote".to_string()),
);
client.bind_inbound_native_admission_stream_contract(
connection_id,
crate::native_protocol::SessionTokenStreamContract::OneShotAdmission,
);
client.bind_outbound_native_admission_stream_contract(
connection_id,
crate::native_protocol::SessionTokenStreamContract::PersistentControl,
);
client.bind_inbound_session_token_transport(connection_id, 10);
client
.remote_session_admission_proofs
.write()
.expect("remote proof registry")
.insert(
connection_id.to_string(),
RemoteSessionAdmissionProof {
transport_stable_id: 10,
token_fingerprint: "token-fingerprint".to_string(),
approval_scope: "user-device".to_string(),
},
);
assert!(client.native_application_stream_admitted_for_transport(connection_id, 10));
assert!(client.invalidate_native_persistent_route_proofs_for_transport(connection_id, 10));
assert!(client.inbound_session_token_admitted_for_transport(connection_id, 10));
assert!(!client.remote_session_token_admitted_for_transport(connection_id, 10));
assert!(!client.native_application_stream_admitted_for_transport(connection_id, 10));
}
#[test]
fn required_application_crypto_blocks_settlement_until_current_key_exists() {
let client = Client::new_with_app_tag(
crate::test_constants::TEST_PROJECT_ID.to_string(),
"test-app".to_string(),
Box::new(|| None),
);
let connection_id = "required-crypto-settlement";
client.set_connection_application_crypto_required(connection_id);
assert_eq!(
client.session_admission_block_reason(connection_id),
Some((false, "application-crypto-key-pending".to_string())),
);
client.set_connection_application_crypto_key(
connection_id,
[19u8; crate::application_crypto::APPLICATION_KEY_BYTES],
);
assert_eq!(client.session_admission_block_reason(connection_id), None);
}
}