#[cfg(test)]
mod tests {
#[cfg(all(
not(target_arch = "wasm32"),
openrtc_iroh_retire_remote_connections_api
))]
use super::super::auto_connect_impl::retire_stale_endpoint_connections;
use super::super::auto_connect_impl::{
active_connection_probe_decision,
active_connection_probe_decision_with_generation_activity,
active_connection_probe_is_healthy, generation_activity_is_recent,
next_active_connection_probe_failure_count, persistent_managed_actor_may_own_transport,
retryable_session_admission_failure_decision, session_admission_presentation_decision,
session_token_presentation_failure_action, session_token_presentation_was_duplicate,
should_auto_present_session_token, superseded_desired_peer_was_withdrawn,
ActiveConnectionProbeDecision, RetryableSessionAdmissionFailureDecision,
SessionTokenPresentationFailureAction,
};
use super::super::core_impl::{
accepted_transport_event_is_current, accepted_transport_replaces_managed_generation,
classify_wasm_closed_transport_rebind, desired_user_device_reopen_candidate,
incoming_transport_requires_carrier_restart, is_duplicate_kept_existing_close_reason,
is_graceful_close_reason, is_manual_disconnect_close_reason,
is_replacement_churn_close_reason, is_terminal_close_reason,
report_iroh_path_observation_if_current, route_repair_needed,
session_admission_timeout_owns_current_transport,
transport_replacement_requires_application_crypto_reconfirmation,
WasmClosedTransportRebind,
};
use super::super::state_signaling_impl::should_present_session_token_on_connected_transport;
use super::super::state_signaling_impl::should_preserve_proven_iroh_carrier_route;
#[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, merge_device_status_snapshots,
next_non_initiator_wakeup_delay_ms, register_auto_connect_admission_rejection,
register_auto_connect_failure, resolve_disable_ipv6, AutoConnectTieBreakDecision,
BleConfig, BridgeAction, CapabilityMaturity, Client, ConnectionStatus,
DevicePresenceStatus, DuplicateClosedHandling, IncomingTransportCloseResolution,
IrohPathKind, ManagedHealth, ProductCapabilityMaturity, ReadinessState,
RemoteSessionAdmissionProof, RuntimeStatus, 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};
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"))]
trait TestTransportGeneration {
fn openrtc_generation(&self) -> u64;
}
#[cfg(not(target_arch = "wasm32"))]
impl TestTransportGeneration for iroh::endpoint::Connection {
fn openrtc_generation(&self) -> u64 {
crate::transport_generation::for_connection(self)
}
}
#[cfg(not(target_arch = "wasm32"))]
#[derive(Debug)]
struct TestBleUpgradeProvider;
#[cfg(not(target_arch = "wasm32"))]
#[async_trait::async_trait]
impl super::super::UpgradeProvider for TestBleUpgradeProvider {
fn kind(&self) -> IrohPathKind {
IrohPathKind::Ble
}
fn transport_id(&self) -> u64 {
0x42_4c_45
}
async fn prepare_endpoint_addr(
&self,
endpoint_id: iroh::EndpointId,
) -> anyhow::Result<iroh::EndpointAddr> {
Ok(iroh::EndpointAddr::new(endpoint_id))
}
}
#[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::TokenStreamContract::PersistentControl,
);
client.bind_outbound_native_admission_stream_contract(
connection_id,
crate::native_protocol::TokenStreamContract::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"))]
struct SyntheticMediaSource {
publication: crate::media::MediaPublicationConfig,
samples: std::collections::VecDeque<crate::media::EncodedMediaSample>,
}
#[cfg(not(target_arch = "wasm32"))]
impl crate::media::MediaSource for SyntheticMediaSource {
fn publication(&self) -> crate::media::MediaPublicationConfig {
self.publication.clone()
}
fn next_sample(
&mut self,
) -> std::pin::Pin<
Box<
dyn std::future::Future<
Output = anyhow::Result<Option<crate::media::EncodedMediaSample>>,
> + Send
+ '_,
>,
> {
Box::pin(async move { Ok(self.samples.pop_front()) })
}
}
#[cfg(not(target_arch = "wasm32"))]
struct CountingMediaSink {
expected_kind: crate::media::MediaKind,
rendered: u64,
last_timestamp_us: u64,
}
#[cfg(not(target_arch = "wasm32"))]
impl crate::media::MediaSink for CountingMediaSink {
fn render(
&mut self,
publication: &crate::media::MediaPublicationConfig,
chunk: crate::media::EncodedMediaChunk,
) -> std::pin::Pin<Box<dyn std::future::Future<Output = anyhow::Result<()>> + Send + '_>>
{
let publication_kind = publication.kind;
Box::pin(async move {
assert_eq!(publication_kind, self.expected_kind);
assert_eq!(chunk.kind, self.expected_kind);
assert!(chunk.timestamp_us >= self.last_timestamp_us);
self.last_timestamp_us = chunk.timestamp_us;
self.rendered = self.rendered.saturating_add(1);
Ok(())
})
}
}
#[cfg(not(target_arch = "wasm32"))]
async fn exchange_native_media_publication(
sender_client: std::sync::Arc<Client>,
receiver_client: std::sync::Arc<Client>,
sender_peer: &str,
receiver_peer: &str,
kind: crate::media::MediaKind,
codec: crate::media::MediaCodec,
publication_seed: u8,
) {
const SAMPLE_COUNT: u64 = 64;
let incoming = receiver_client
.incoming_streams()
.await
.expect("native media incoming subscription");
let publication = crate::media::MediaPublicationConfig {
publication_id: crate::media::PublicationId([publication_seed; 16]),
media_generation: 1,
kind,
codec,
clock_rate: if kind == crate::media::MediaKind::Audio {
48_000
} else {
1_000_000
},
coded_width: (kind == crate::media::MediaKind::Video).then_some(320),
coded_height: (kind == crate::media::MediaKind::Video).then_some(180),
channels: (kind == crate::media::MediaKind::Audio).then_some(1),
};
let samples = (0..SAMPLE_COUNT)
.map(|sequence| crate::media::EncodedMediaSample {
timestamp_us: sequence * 20_000,
duration_us: 20_000,
keyframe: kind == crate::media::MediaKind::Audio || sequence % 30 == 0,
discardable: kind == crate::media::MediaKind::Video && sequence % 30 != 0,
payload: [publication_seed, sequence as u8].repeat(32),
})
.collect();
let source = SyntheticMediaSource {
publication,
samples,
};
let sender_media = crate::media::MediaConnection::for_capability(
sender_client,
sender_peer,
"ticket",
"native-media-test",
)
.expect("sender media capability");
let receiver_media = crate::media::MediaConnection::for_capability(
receiver_client,
receiver_peer,
"ticket",
"native-media-test",
)
.expect("receiver media capability");
let mut sender = sender_media
.add_track(Box::new(source))
.await
.expect("open native media sender");
let stream = tokio::time::timeout(Duration::from_secs(3), incoming.recv())
.await
.expect("native media incoming timeout")
.expect("native media incoming stream");
let mut receiver = receiver_media
.accept_incoming(stream)
.await
.expect("accept native media publication");
let mut sink = CountingMediaSink {
expected_kind: kind,
rendered: 0,
last_timestamp_us: 0,
};
for _ in 0..SAMPLE_COUNT {
assert!(sender.send_next().await.expect("send native media sample"));
assert!(
tokio::time::timeout(Duration::from_secs(2), receiver.render_next(&mut sink))
.await
.expect("render native media timeout")
.expect("render native media sample")
);
}
assert!(!sender.send_next().await.expect("native source completion"));
assert_eq!(sender.stats().transmitted, SAMPLE_COUNT);
assert_eq!(sender.stats().queued, 0);
assert_eq!(receiver.stats().rendered, SAMPLE_COUNT);
assert_eq!(sink.rendered, SAMPLE_COUNT);
sender.stop().expect("stop native media sender");
}
#[test]
fn transient_network_reconnect_reason_is_not_terminal() {
let reason = "ApplicationClosed(0, b\"network-change-forced-reconnect\")";
assert!(crate::lifecycle_reason::is_transient(Some(reason)));
assert!(!crate::lifecycle_reason::is_graceful(Some(reason)));
assert!(!crate::lifecycle_reason::is_manual_disconnect(Some(reason)));
let carrier_policy = "ApplicationClosed(0, b\"iroh-carrier-policy-changed\")";
assert!(crate::lifecycle_reason::is_transient(Some(carrier_policy)));
assert!(!crate::lifecycle_reason::is_terminal(Some(carrier_policy)));
}
#[cfg(all(
not(target_arch = "wasm32"),
openrtc_iroh_retire_remote_connections_api
))]
#[tokio::test]
async fn runtime_restart_cleanup_preserves_the_exact_iroh_control_connection() {
const CONTROL_ALPN: &[u8] = b"openrtc/control/runtime-restart-id-fence";
let local = Endpoint::builder(iroh::endpoint::presets::Minimal)
.relay_mode(RelayMode::Disabled)
.alpns(vec![CONTROL_ALPN.to_vec()])
.bind()
.await
.expect("bind local endpoint");
let remote = Endpoint::builder(iroh::endpoint::presets::Minimal)
.relay_mode(RelayMode::Disabled)
.alpns(vec![CONTROL_ALPN.to_vec()])
.bind()
.await
.expect("bind remote endpoint");
let (stale_local, stale_remote) =
tokio::join!(local.connect(remote.addr(), CONTROL_ALPN), async {
remote.accept().await.expect("stale incoming").await
},);
let stale_local = stale_local.expect("open stale connection");
let stale_remote = stale_remote.expect("accept stale connection");
let (current_local, current_remote) =
tokio::join!(local.connect(remote.addr(), CONTROL_ALPN), async {
remote.accept().await.expect("current incoming").await
},);
let current_local = current_local.expect("open current connection");
let current_remote = current_remote.expect("accept current connection");
let openrtc_generation = crate::transport_generation::for_connection(¤t_local);
assert_ne!(
openrtc_generation,
current_local.stable_id() as u64,
"the regression requires distinct OpenRTC and Iroh identifiers",
);
let retired = retire_stale_endpoint_connections(
&local,
remote.id(),
¤t_local,
b"runtime-instance-replaced",
)
.await;
assert_eq!(retired, 1, "only the stale connection should retire");
tokio::time::timeout(Duration::from_secs(5), stale_local.closed())
.await
.expect("stale connection was not retired");
assert!(
tokio::time::timeout(Duration::from_millis(250), current_local.closed())
.await
.is_err(),
"the admitted control connection must remain live",
);
current_local.close(0u32.into(), b"test-complete");
stale_remote.close(0u32.into(), b"test-complete");
current_remote.close(0u32.into(), b"test-complete");
tokio::join!(local.close(), remote.close());
}
#[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",
"authoritative-device-binding-pending",
] {
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), false),
RetryableSessionAdmissionFailureDecision::PreserveCurrentTransport,
);
assert_eq!(
retryable_session_admission_failure_decision(2, 2, Some(41), Some(41), false),
RetryableSessionAdmissionFailureDecision::RetireCurrentTransport,
);
}
#[test]
fn retryable_admission_failure_cannot_retire_replacement_generation() {
assert_eq!(
retryable_session_admission_failure_decision(2, 2, Some(41), Some(42), false),
RetryableSessionAdmissionFailureDecision::ReplacementAlreadyWon,
);
assert_eq!(
retryable_session_admission_failure_decision(2, 2, Some(41), None, false),
RetryableSessionAdmissionFailureDecision::ReplacementAlreadyWon,
);
}
#[test]
fn failed_admission_after_remote_runtime_restart_retires_stale_generation_immediately() {
assert_eq!(
retryable_session_admission_failure_decision(1, 2, Some(41), Some(41), true),
RetryableSessionAdmissionFailureDecision::RetireCurrentTransport,
);
assert_eq!(
retryable_session_admission_failure_decision(1, 2, Some(41), Some(42), true),
RetryableSessionAdmissionFailureDecision::ReplacementAlreadyWon,
"a late failure must never retire the replacement generation",
);
}
#[test]
fn superseded_desired_peer_attempt_only_retires_an_authoritatively_withdrawn_peer() {
assert!(
superseded_desired_peer_was_withdrawn(true, 12, 11, false),
"a newer roster that removed this device owns its retirement",
);
assert!(
!superseded_desired_peer_was_withdrawn(true, 12, 11, true),
"another peer's revision must preserve this still-desired transport",
);
assert!(
!superseded_desired_peer_was_withdrawn(false, 12, 11, false),
"an inactive or replaced actor cannot retire shared transport",
);
assert!(
!superseded_desired_peer_was_withdrawn(true, 11, 11, false),
"the attempt's own revision is not superseding evidence",
);
}
#[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 scoped_session_transport_does_not_require_a_user_device_reopen() {
assert_eq!(
desired_user_device_reopen_candidate(true, None),
None,
"a scoped grant must reach session-token admission without appearing in user-device discovery",
);
assert_eq!(
desired_user_device_reopen_candidate(true, Some("device-b")),
Some("device-b"),
);
assert_eq!(
desired_user_device_reopen_candidate(false, Some("device-b")),
None,
);
}
#[test]
fn accepted_transport_replacement_requires_prior_different_generation() {
assert!(accepted_transport_replaces_managed_generation(
Some(41),
42,
1,
false
));
assert!(!accepted_transport_replaces_managed_generation(
Some(42),
42,
1,
false
));
assert!(accepted_transport_replaces_managed_generation(
None, 42, 2, false
));
assert!(accepted_transport_replaces_managed_generation(
None, 42, 1, true
));
assert!(!accepted_transport_replaces_managed_generation(
None, 42, 1, false
));
}
#[test]
fn incoming_transport_restarts_carrier_preparation_for_new_or_replaced_generations() {
assert!(incoming_transport_requires_carrier_restart(false, false));
assert!(incoming_transport_requires_carrier_restart(false, true));
assert!(incoming_transport_requires_carrier_restart(true, true));
assert!(!incoming_transport_requires_carrier_restart(true, false));
}
#[test]
fn incoming_user_device_replacement_wakes_route_repair_without_a_roster_change() {
assert!(route_repair_needed(true, true));
assert!(
!route_repair_needed(false, true),
"an unknown or transient peer must not enter managed user-device repair",
);
assert!(
!route_repair_needed(true, false),
"an already-proven current generation needs no duplicate presentation",
);
}
#[test]
fn wasm_close_rebind_requires_a_concrete_base_transport_generation() {
assert_eq!(
classify_wasm_closed_transport_rebind(false, Some(42), None, true),
WasmClosedTransportRebind::BaseReplacement(42),
);
assert_eq!(
classify_wasm_closed_transport_rebind(false, Some(42), Some(42), true),
WasmClosedTransportRebind::AlreadyPromoted(42),
"a delayed incumbent close cannot republish the committed carrier generation",
);
assert_eq!(
classify_wasm_closed_transport_rebind(false, Some(42), Some(41), true),
WasmClosedTransportRebind::BaseReplacement(42),
"a different managed generation still needs the concrete replacement promotion",
);
assert_eq!(
classify_wasm_closed_transport_rebind(false, None, None, true),
WasmClosedTransportRebind::SurvivingCarrierAwaitingReplacement,
);
assert_eq!(
classify_wasm_closed_transport_rebind(false, None, None, false),
WasmClosedTransportRebind::NoLiveTransport,
);
assert_eq!(
classify_wasm_closed_transport_rebind(true, Some(42), Some(42), true),
WasmClosedTransportRebind::TerminalDisconnect,
"terminal security closes must outrank every surviving transport",
);
}
#[test]
fn base_observations_do_not_downgrade_a_proven_iroh_carrier_route() {
assert!(should_preserve_proven_iroh_carrier_route(
"ble",
"iroh-relay",
));
assert!(should_preserve_proven_iroh_carrier_route(
"ble",
"iroh-quic",
));
assert!(should_preserve_proven_iroh_carrier_route(
"webrtc",
"iroh-relay",
));
assert!(should_preserve_proven_iroh_carrier_route(
"moq",
"iroh-quic",
));
assert!(!should_preserve_proven_iroh_carrier_route(
"iroh-relay",
"iroh-quic",
));
assert!(should_preserve_proven_iroh_carrier_route(
"webrtc", "webrtc",
));
}
#[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));
let mut relay_disabled = current.clone();
relay_disabled.relay = false;
assert!(current.endpoint_rebind_required(&relay_disabled));
}
#[test]
fn relay_disabled_sanitizes_subordinate_carriers() {
let config = TransportConfig {
relay: false,
privacy_mode: true,
iroh_relay_only: true,
moq: Some(crate::client::MoQConfig {
relay_url: "https://relay.example.test".to_string(),
..crate::client::MoQConfig::default()
}),
webrtc: Some(WebRTCConfig {
ice_servers: vec![super::super::IceServerConfig {
urls: vec![
"stun:stun.example.test:3478".to_string(),
"turns:turn.example.test:5349".to_string(),
],
username: Some("user".to_string()),
credential: Some("secret".to_string()),
}],
..WebRTCConfig::default()
}),
..TransportConfig::default()
}
.sanitize_for_runtime();
assert!(!config.relay);
assert!(!config.privacy_mode);
assert!(!config.iroh_relay_only);
assert!(config.moq.is_none());
assert!(!config
.route_priority
.contains(&crate::route_policy::KnownRoute::IrohRelay));
assert!(!config
.route_priority
.contains(&crate::route_policy::KnownRoute::Moq));
assert!(!config
.route_priority
.contains(&crate::route_policy::KnownRoute::Moq));
assert_eq!(
config
.webrtc
.expect("WebRTC carrier remains available")
.ice_servers[0]
.urls,
vec!["stun:stun.example.test:3478".to_string()],
);
}
#[test]
fn local_only_is_a_hard_endpoint_and_route_policy() {
let config = TransportConfig {
network_policy: super::super::NetworkPolicy::LocalOnly,
relay: true,
privacy_mode: true,
iroh_relay_only: true,
iroh_relay_transport_policy: Some(super::super::RelayPolicy::Auto),
moq: Some(crate::client::MoQConfig {
relay_url: "https://relay.example.test".to_string(),
..crate::client::MoQConfig::default()
}),
webrtc: Some(WebRTCConfig::default()),
..TransportConfig::default()
}
.sanitize_for_runtime();
assert_eq!(
config.network_policy,
super::super::NetworkPolicy::LocalOnly
);
assert!(!config.relay);
assert!(!config.privacy_mode);
assert!(!config.iroh_relay_only);
assert!(config.iroh_relay_transport_policy.is_none());
assert!(config.moq.is_none());
assert!(config.webrtc.is_none());
assert!(config.iroh_lan.as_ref().is_some_and(|lan| lan.enabled));
assert!(config.route_priority.iter().all(|route| matches!(
route,
crate::route_policy::KnownRoute::IrohLan
| crate::route_policy::KnownRoute::IrohQuic
| crate::route_policy::KnownRoute::Ble
)));
let mut managed = config.clone();
managed.network_policy = super::super::NetworkPolicy::Managed;
assert!(config.endpoint_rebind_required(&managed));
}
#[test]
fn legacy_implementation_fields_are_ignored() {
let webrtc: WebRTCConfig = serde_json::from_value(serde_json::json!({
"implementation": "external"
}))
.unwrap();
let moq: crate::client::MoQConfig = serde_json::from_value(serde_json::json!({
"implementation": "auto",
"relayUrl": "https://relay.example.test"
}))
.unwrap();
assert!(serde_json::to_value(webrtc)
.unwrap()
.get("implementation")
.is_none());
assert!(serde_json::to_value(moq)
.unwrap()
.get("implementation")
.is_none());
}
#[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_provider_neutral("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"));
}
#[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);
assert_eq!(
client.product_capability_maturity(),
ProductCapabilityMaturity {
bounded_rooms: CapabilityMaturity::Stable,
service_nodes: CapabilityMaturity::Preview,
offline_edge: if cfg!(feature = "transport-lan") {
CapabilityMaturity::Preview
} else {
CapabilityMaturity::SupportOnly
},
broadcast: CapabilityMaturity::Preview,
}
);
let RuntimeStatus {
ready: _,
node_id: _,
transport: _,
} = status;
}
#[cfg(not(feature = "transport-lan"))]
#[test]
fn native_offline_maturity_is_support_only_without_lan() {
let client = Client::new_provider_neutral("test-app".to_string(), Box::new(|| None));
assert_eq!(
client.product_capability_maturity().offline_edge,
CapabilityMaturity::SupportOnly
);
}
#[cfg(feature = "transport-lan")]
#[test]
fn native_offline_maturity_is_preview_with_lan() {
let client = Client::new_provider_neutral("test-app".to_string(), Box::new(|| None));
assert_eq!(
client.product_capability_maturity().offline_edge,
CapabilityMaturity::Preview
);
}
#[tokio::test]
async fn builder_preserves_ble_intent_until_native_host_registers_provider() {
let client = super::super::ClientBuilder::new_with_app_tag(
crate::test_constants::TEST_PROJECT_ID.to_string(),
"test-app".to_string(),
Box::new(|| None),
)
.transport_config(TransportConfig {
ble: Some(BleConfig {
enabled: true,
connect_timeout_ms: Some(20_000),
}),
..TransportConfig::default()
})
.build();
assert!(client
.transport_config()
.await
.ble
.is_some_and(|config| config.enabled));
let status = client.runtime_status().await;
assert!(!status.transport.ble.compiled);
assert!(!status.transport.ble.enabled);
client
.add_upgrade_provider(std::sync::Arc::new(TestBleUpgradeProvider))
.await
.expect("native host should register its deferred BLE provider");
assert!(client.is_ble_transport_enabled().await);
let status = client.runtime_status().await;
assert!(status.transport.ble.compiled);
assert!(status.transport.ble.enabled);
}
#[tokio::test]
async fn relay_only_transport_config_fails_closed_across_routes() {
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 {
privacy_mode: true,
iroh_relay_only: true,
iroh_lan: Some(super::super::IrohLanConfig::default()),
webrtc: Some(WebRTCConfig {
privacy_mode: false,
lan_mode: true,
ice_servers: vec![super::super::IceServerConfig {
urls: vec![
"stun:stun.example.test:3478".to_string(),
"turns:turn.example.test:5349".to_string(),
],
username: Some("user".to_string()),
credential: Some("secret".to_string()),
}],
}),
route_priority: vec![
crate::route_policy::KnownRoute::IrohLan,
crate::route_policy::KnownRoute::IrohQuic,
crate::route_policy::KnownRoute::WebRtc,
crate::route_policy::KnownRoute::IrohRelay,
],
..TransportConfig::default()
})
.await
.expect("relay-only policy should sanitize direct inputs");
let config = client.transport_config().await;
assert!(config.iroh_lan.is_none());
assert!(config.ble.is_none());
assert_eq!(
config.route_priority,
vec![
crate::route_policy::KnownRoute::WebRtc,
crate::route_policy::KnownRoute::IrohRelay,
]
);
let webrtc = config.webrtc.expect("relay-safe WebRTC config");
assert!(webrtc.privacy_mode);
assert!(!webrtc.lan_mode);
assert_eq!(
webrtc.ice_servers[0].urls,
vec!["turns:turn.example.test:5349".to_string()]
);
}
#[tokio::test]
async fn iroh_relay_only_does_not_silently_enable_global_privacy() {
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 {
iroh_relay_only: true,
webrtc: Some(WebRTCConfig {
lan_mode: true,
..WebRTCConfig::default()
}),
..TransportConfig::default()
})
.await
.expect("Iroh-only relay policy");
let config = client.transport_config().await;
assert!(!config.privacy_mode);
assert!(config.iroh_relay_only);
assert!(config
.webrtc
.as_ref()
.is_some_and(|webrtc| !webrtc.privacy_mode && webrtc.lan_mode));
assert_eq!(
config.ranked_iroh_carriers(true, true),
vec![
crate::route_policy::KnownRoute::WebRtc,
crate::route_policy::KnownRoute::Moq,
],
);
}
#[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);
}
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 current_generation_activity_prevents_fallback_probe_from_demoting_peer() {
assert_eq!(
active_connection_probe_decision_with_generation_activity(false, false, false),
ActiveConnectionProbeDecision::Failed
);
}
#[test]
fn recent_current_generation_activity_prevents_probe_false_negative() {
assert!(generation_activity_is_recent(
Some(std::time::Duration::from_millis(8_999)),
5_000,
));
assert!(!generation_activity_is_recent(
Some(std::time::Duration::from_millis(9_001)),
5_000,
));
assert!(!generation_activity_is_recent(None, 5_000));
assert_eq!(
active_connection_probe_decision_with_generation_activity(false, true, true),
ActiveConnectionProbeDecision::Healthy,
"peer-initiated traffic on the exact physical generation is positive liveness evidence",
);
assert_eq!(
active_connection_probe_decision_with_generation_activity(false, false, true),
ActiveConnectionProbeDecision::Healthy,
"current-generation protocol traffic remains positive proof when the independent ping returns no live probe object",
);
}
#[cfg(not(target_arch = "wasm32"))]
#[test]
fn protocol_activity_is_scoped_to_and_retired_with_its_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 = "protocol-activity-generation";
client.record_native_transport_protocol_activity(connection_id, 41);
assert!(
generation_activity_is_recent(
client.native_transport_protocol_activity_age(connection_id, 41),
5_000,
),
"valid protocol activity is positive liveness evidence for its exact transport",
);
assert_eq!(
client.native_transport_protocol_activity_age(connection_id, 42),
None,
"an old physical generation cannot keep its replacement alive",
);
client.invalidate_native_main_route_proofs_for_transport(connection_id, 41);
assert_eq!(
client.native_transport_protocol_activity_age(connection_id, 41),
None,
"transport invalidation retires its liveness evidence even when no admission proof existed",
);
client.record_native_transport_protocol_activity(connection_id, 43);
client.forget_session_connection(connection_id);
assert_eq!(
client.native_transport_protocol_activity_age(connection_id, 43),
None,
"logical-session retirement removes all protocol activity",
);
}
#[test]
fn repeated_active_probe_misses_retire_a_passively_open_transport() {
let decision = active_connection_probe_decision(false, true);
let first_miss = next_active_connection_probe_failure_count(decision, 0);
assert_eq!(first_miss, 1);
assert!(
active_connection_probe_is_healthy(decision, first_miss, 2),
"one missed ping may be transient while the owned transport settles"
);
let second_miss = next_active_connection_probe_failure_count(decision, first_miss);
assert_eq!(second_miss, 2);
assert!(
!active_connection_probe_is_healthy(decision, second_miss, 2),
"a passively open handle is not positive liveness evidence after repeated ping misses"
);
assert_eq!(
next_active_connection_probe_failure_count(
ActiveConnectionProbeDecision::Healthy,
second_miss,
),
0,
"a successful current-generation probe clears prior misses"
);
}
#[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, true);
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, true);
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 observing_replacement_without_proofs =
session_admission_presentation_decision(true, false, false, false, false);
assert!(
!observing_replacement_without_proofs.should_present,
"the observing peer must not race the designated initiator's persistent stream",
);
assert!(!observing_replacement_without_proofs.request_reciprocal);
let peer_requested = session_admission_presentation_decision(true, true, true, true, false);
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, false);
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, true);
assert!(!settled.should_present);
assert!(!settled.request_reciprocal);
let tokenless = session_admission_presentation_decision(false, true, false, true, true);
assert!(!tokenless.should_present);
assert!(!tokenless.request_reciprocal);
}
#[test]
fn persistent_user_device_actor_yields_to_explicit_capability_scopes() {
let user_device = vec!["user-device".to_string()];
assert!(persistent_managed_actor_may_own_transport(&[], None, false));
assert!(persistent_managed_actor_may_own_transport(
&user_device,
Some("user-device"),
false,
));
for explicit_scope in [
"drive-grant:grant-1",
"room:match-1",
"space:portfolio-cursors",
"ticket:one-shot",
] {
assert!(
!persistent_managed_actor_may_own_transport(
&[explicit_scope.to_string()],
None,
false,
),
"the background user-device actor must yield to {explicit_scope}",
);
assert!(
!persistent_managed_actor_may_own_transport(
&user_device,
Some(explicit_scope),
false,
),
"the accepted admission scope is authoritative before the projection catches up",
);
}
assert!(
!persistent_managed_actor_may_own_transport(&user_device, None, true),
"a concurrent explicit presentation owns the transport until its guard releases",
);
}
#[cfg(not(target_arch = "wasm32"))]
#[tokio::test]
async fn reciprocal_admission_request_is_owned_wakes_repair_and_retires_with_the_session() {
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 connection_id = "reciprocal-repair-connection";
assert!(
!client
.request_reciprocal_session_admission(connection_id)
.await,
"the request is retained even before the external actor starts",
);
client
.start_external_auto_connect("test-user".to_string(), "local-device".to_string())
.await
.expect("external actor starts");
assert!(
client
.request_reciprocal_session_admission(connection_id)
.await,
"a repeated request on a replacement generation must wake the repair owner",
);
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));
}
#[cfg(not(target_arch = "wasm32"))]
#[tokio::test]
async fn reciprocal_admission_request_wakes_provider_backed_auto_connect_owner() {
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 connection_id = "provider-reciprocal-repair-connection";
*client
.auto_connect_loop_key
.lock()
.unwrap_or_else(|poisoned| poisoned.into_inner()) =
Some(("test-user".to_string(), "local-device".to_string()));
assert!(
client
.request_reciprocal_session_admission(connection_id)
.await,
"the provider-backed actor must accept generation-local admission input",
);
tokio::time::timeout(
std::time::Duration::from_millis(50),
client.native_auto_connect_wake.notified(),
)
.await
.expect("the provider-backed actor wake must retain a permit");
}
#[cfg(not(target_arch = "wasm32"))]
#[test]
fn managed_connection_ticket_owns_ble_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),
);
let remote_node_id = "managed-route-repair-node";
client.update_native_route_repair_credential(
remote_node_id,
Some("managed-route-repair-device"),
Some("managed-route-repair-token"),
Some("managed-route-repair-payload"),
);
let credential = client
.native_route_repair_credentials
.read()
.unwrap()
.get(remote_node_id)
.cloned()
.expect("credential should be retained for BLE route replacement");
assert_eq!(
credential.authoritative_device_id,
"managed-route-repair-device"
);
assert_eq!(credential.token, "managed-route-repair-token");
assert_eq!(credential.token_payload, "managed-route-repair-payload");
client.update_native_route_repair_credential(remote_node_id, None, None, None);
assert!(
!client
.native_route_repair_credentials
.read()
.unwrap()
.contains_key(remote_node_id),
"a direct or invalid replacement ticket must retire the stale credential"
);
}
#[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
);
}
#[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_close_reason(Some(
"ApplicationClosed(0, b\"closed\")"
)));
assert!(is_graceful_close_reason(Some(
"ApplicationClosed(1, b\"disconnected by user\")"
)));
assert!(is_graceful_close_reason(Some("manual disconnect")));
assert!(!is_graceful_close_reason(Some(
"ApplicationClosed(0, b\"duplicate-kept-existing\")"
)));
assert!(!is_graceful_close_reason(Some(
"ApplicationClosed(1, b\"session-token-revoked\")"
)));
assert!(!is_graceful_close_reason(Some("transport reset by peer")));
assert!(!is_graceful_close_reason(None));
}
#[test]
fn terminal_disconnect_close_reason_settles_authorization_failures() {
assert!(is_terminal_close_reason(Some(
"ApplicationClosed(0, b\"closed\")"
)));
assert!(is_terminal_close_reason(Some(
"ApplicationClosed(1, b\"session-token-revoked\")"
)));
assert!(is_terminal_close_reason(Some(
"ApplicationClosed(1, b\"session-admission-rejected\")"
)));
assert!(!is_terminal_close_reason(Some(
"ApplicationClosed(0, b\"duplicate-kept-existing\")"
)));
assert!(!is_terminal_close_reason(Some("transport reset by peer")));
assert!(!is_terminal_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));
}
#[tokio::test]
async fn logical_session_terminal_snapshot_uses_current_owner_verdict() {
use super::super::{connection_state_snapshot_from_parts, peer_session_snapshot_from_peer};
let manager = crate::connection_manager::ConnectionManager::new();
for (index, reason) in ["session-token-revoked", "desired-peer-withdrawn", "closed"]
.into_iter()
.enumerate()
{
let id = format!("terminal-{index}");
manager
.upsert_pending(id.clone(), Some(id.clone()), None, None)
.await;
let closed = manager
.set_closed(&id, Some(reason.to_string()))
.await
.unwrap();
let peer = manager.peer_snapshot(&id).await.unwrap();
assert!(peer.logical_session_terminal);
let snapshot = connection_state_snapshot_from_parts(&closed, Some(&peer));
assert!(snapshot.logical_session_terminal);
assert_eq!(snapshot.state, "closed");
assert_eq!(snapshot.error.as_deref(), Some(reason));
assert_eq!(
serde_json::to_value(&snapshot).unwrap()["logicalSessionTerminal"],
true
);
let session = peer_session_snapshot_from_peer(peer);
assert!(session.logical_session_terminal);
assert_eq!(
serde_json::to_value(&session).unwrap()["logicalSessionTerminal"],
true
);
let reauthorized = manager.set_connecting(&id).await.unwrap();
let peer = manager.peer_snapshot(&id).await.unwrap();
assert!(
!connection_state_snapshot_from_parts(&closed, Some(&peer))
.logical_session_terminal
);
assert!(!connection_state_snapshot_from_parts(&closed, None).logical_session_terminal);
assert!(
!connection_state_snapshot_from_parts(&reauthorized, Some(&peer))
.logical_session_terminal
);
assert!(!peer_session_snapshot_from_peer(peer).logical_session_terminal);
}
}
#[tokio::test]
async fn logical_session_terminal_snapshot_excludes_superseded_records() {
use super::super::{connection_state_snapshot_from_parts, peer_session_snapshot_from_peer};
let manager = crate::connection_manager::ConnectionManager::new();
manager
.upsert_pending("old".into(), Some("peer".into()), None, None)
.await;
let replaced = manager
.set_closed("old", Some("replaced-by-new-inbound".into()))
.await
.unwrap();
let peer = manager.peer_snapshot("peer").await.unwrap();
let snapshot = connection_state_snapshot_from_parts(&replaced, Some(&peer));
assert_eq!(snapshot.state, "closed");
assert!(!snapshot.replacement_in_progress);
assert!(!snapshot.logical_session_terminal);
let old = manager
.set_closed("old", Some("session-token-revoked".into()))
.await
.unwrap();
manager
.upsert_pending("successor".into(), Some("peer".into()), None, None)
.await;
let successor = manager.set_connecting("successor").await.unwrap();
assert_eq!(old.transport_generation, successor.transport_generation);
assert_eq!(old.route_generation, successor.route_generation);
let peer = manager.peer_snapshot("peer").await.unwrap();
assert_eq!(
peer.connection_ids.first().map(String::as_str),
Some("successor")
);
assert!(!connection_state_snapshot_from_parts(&old, Some(&peer)).logical_session_terminal);
assert!(!peer_session_snapshot_from_peer(peer).logical_session_terminal);
}
#[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 {
logical_session_terminal: false,
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 {
logical_session_terminal: false,
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, ConnectionStatus::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 {
logical_session_terminal: false,
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, ConnectionStatus::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 {
logical_session_terminal: false,
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 {
logical_session_terminal: false,
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, ConnectionStatus::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 {
logical_session_terminal: false,
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 {
logical_session_terminal: false,
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, ConnectionStatus::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 {
logical_session_terminal: false,
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 {
logical_session_terminal: false,
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,
ConnectionStatus::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 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 {
logical_session_terminal: false,
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 {
logical_session_terminal: false,
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,
ConnectionStatus::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, ConnectionStatus::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::coordination::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::presence_policy::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, ConnectionStatus::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, ConnectionStatus::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, ConnectionStatus::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 local_manual_disconnect_is_authoritative_over_online_discovery() {
let client = Client::new_with_app_tag(
crate::test_constants::TEST_PROJECT_ID.to_string(),
"test-app".to_string(),
Box::new(|| None),
);
let device = 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 snapshot = merge_device_status_snapshots(vec![device], Vec::new())
.into_iter()
.next()
.expect("snapshot");
client.set_auto_connect_excluded("device-1", true);
let snapshot = client.apply_local_manual_disconnect_status(snapshot);
assert_eq!(snapshot.connection_status, ConnectionStatus::Closed);
assert_eq!(snapshot.readiness_state, ReadinessState::Closed);
assert_eq!(snapshot.readiness_reason, "manual disconnect");
assert!(!snapshot.settled_ready);
assert!(snapshot.connectable);
assert!(snapshot.connection_id.is_none());
}
#[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_eq!(
client.auto_connect_exclusion_for_peer(None, Some("node-peer")),
Some(true),
"the node alias must retain the peer-requested tombstone after the active row retires",
);
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_eq!(
client.auto_connect_exclusion_for_peer(None, Some("node-local")),
Some(false),
"a local tombstone must remain distinguishable through its node alias",
);
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 response_ack_manual_reconnect_requires_authoritative_user_device_admission() {
use crate::session_token::{AdmissionMechanism, GrantScope, SessionAdmission};
let accepted = SessionAdmission::Accepted {
mechanism: AdmissionMechanism::SessionToken,
scope: Some(GrantScope::new("user-device")),
authoritative_device_id: Some("device-peer".to_string()),
};
assert_eq!(
super::super::admission_impl::authenticated_user_device_for_response_ack(&accepted),
Some("device-peer"),
);
assert!(
super::super::admission_impl::response_ack_claimed_device_matches_admission(
&accepted,
Some("device-peer"),
)
);
assert!(
!super::super::admission_impl::response_ack_claimed_device_matches_admission(
&accepted,
Some("device-other"),
)
);
assert!(
!super::super::admission_impl::response_ack_claimed_device_matches_admission(
&accepted, None,
)
);
for refused in [
SessionAdmission::Accepted {
mechanism: AdmissionMechanism::SessionToken,
scope: Some(GrantScope::new("trusted-device")),
authoritative_device_id: Some("device-peer".to_string()),
},
SessionAdmission::Accepted {
mechanism: AdmissionMechanism::SessionToken,
scope: Some(GrantScope::new("user-device")),
authoritative_device_id: None,
},
SessionAdmission::Rejected {
reason: crate::lifecycle_reason::REASON_SESSION_TOKEN_REVOKED.to_string(),
},
] {
assert!(
super::super::admission_impl::authenticated_user_device_for_response_ack(&refused)
.is_none()
);
}
}
#[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"));
let security_committed = std::sync::atomic::AtomicBool::new(false);
assert!(
!client.commit_peer_requested_reconnect_if_authenticated("device-peer", || {
security_committed.store(true, std::sync::atomic::Ordering::SeqCst);
true
},)
);
assert!(
!security_committed.load(std::sync::atomic::Ordering::SeqCst),
"a local manual exclusion must veto security mutation, not merely manager publication",
);
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);
let security_committed = std::sync::atomic::AtomicBool::new(false);
assert!(
!client.commit_peer_requested_reconnect_if_authenticated("device-peer", || {
security_committed.store(true, std::sync::atomic::Ordering::SeqCst);
true
},)
);
assert!(!security_committed.load(std::sync::atomic::Ordering::SeqCst));
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 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_ticket(
peer_a
.endpoint_ticket()
.await
.expect("peer a ticket")
.as_str(),
token_a,
"user-device",
0,
);
let ticket_b = crate::session_token::build_ticket(
peer_b
.endpoint_ticket()
.await
.expect("peer b ticket")
.as_str(),
token_b,
"user-device",
0,
);
let (_, token_a_payload) = crate::session_token::split_ticket(&ticket_a);
let (_, token_b_payload) = crate::session_token::split_ticket(&ticket_b);
*peer_a
.auto_connect_loop_key
.lock()
.expect("peer a identity") =
Some(("user-1".to_string(), "crossed-device-a".to_string()));
*peer_b
.auto_connect_loop_key
.lock()
.expect("peer b identity") =
Some(("user-1".to_string(), "crossed-device-b".to_string()));
peer_a.observe_authoritative_device_node("crossed-device-b", &endpoint_b_id.to_string());
peer_b.observe_authoritative_device_node("crossed-device-a", &endpoint_a_id.to_string());
peer_a
.native_route_repair_credentials
.write()
.expect("peer a route-repair credential")
.insert(
endpoint_b_id.to_string(),
crate::client::NativeRouteRepairCredential {
token: token_b.to_string(),
token_payload: token_b_payload.expect("peer b token payload").to_string(),
authoritative_device_id: "crossed-device-b".to_string(),
},
);
peer_b
.native_route_repair_credentials
.write()
.expect("peer b route-repair credential")
.insert(
endpoint_a_id.to_string(),
crate::client::NativeRouteRepairCredential {
token: token_a.to_string(),
token_payload: token_a_payload.expect("peer a token payload").to_string(),
authoritative_device_id: "crossed-device-a".to_string(),
},
);
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_peer("crossed-device-b", Some(2_000)),
peer_b.wait_for_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")
.openrtc_generation();
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")
.openrtc_generation();
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_admitted_device(&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_ticket("remote-ticket", token, "user-device", 0);
let (_, token_payload) = crate::session_token::split_ticket(&compound);
client
.prepare_reciprocal_admission(
remote_endpoint_id,
stable_id,
"stream-current",
"presentation-current",
token,
token_payload.expect("token payload"),
"local-device",
crate::native_protocol::TokenStreamContract::OneShotAdmission,
)
.await
.expect("current transcript must prepare");
client
.confirm_reciprocal_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_reciprocal_admission(
remote_endpoint_id,
stable_id,
"stream-stale",
"presentation-stale",
token,
token_payload.expect("token payload"),
"local-device",
crate::native_protocol::TokenStreamContract::OneShotAdmission,
)
.await
.expect("second transcript must prepare");
let wrong_stream = client
.confirm_reciprocal_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_reciprocal_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_reciprocal_admission(
remote_endpoint_id,
stable_id,
"stream-scope",
"presentation-scope",
token,
token_payload.expect("token payload"),
"local-device",
crate::native_protocol::TokenStreamContract::OneShotAdmission,
)
.await
.expect("scope transcript must prepare");
assert!(
client
.confirm_reciprocal_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_reciprocal_admission(
remote_endpoint_id,
stable_id,
"stream-epoch",
"presentation-epoch",
token,
token_payload.expect("token payload"),
"local-device",
crate::native_protocol::TokenStreamContract::OneShotAdmission,
)
.await
.expect("epoch transcript must prepare");
client.session_token_registry.mark_accepted(
&connection_id,
crate::session_token::AdmissionMechanism::SessionToken,
Some(crate::session_token::GrantScope::from("user-device")),
Some("remote-device".to_string()),
);
assert!(
client
.confirm_reciprocal_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_reciprocal_admission(
remote_endpoint_id,
stable_id,
"stream-local-identity",
"presentation-local-identity",
token,
token_payload.expect("token payload"),
"local-device",
crate::native_protocol::TokenStreamContract::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_reciprocal_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_reciprocal_admission(
remote_endpoint_id,
stable_id,
"stream-remote-identity",
"presentation-remote-identity",
token,
token_payload.expect("token payload"),
"local-device",
crate::native_protocol::TokenStreamContract::OneShotAdmission,
)
.await
.expect("remote identity transcript must prepare");
client.observe_authoritative_device_node(
"replacement-remote-device",
&remote_endpoint_id.to_string(),
);
assert!(
client
.confirm_reciprocal_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_reciprocal_admission(
remote_endpoint_id,
stable_id,
"stream-logout",
"presentation-logout",
token,
token_payload.expect("token payload"),
"local-device",
crate::native_protocol::TokenStreamContract::OneShotAdmission,
)
.await
.expect("logout transcript must prepare");
client.clear_session_tokens();
assert!(
client
.confirm_reciprocal_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_admitted_device(&connection_id, "remote-device",)
.await
);
client
.prepare_reciprocal_admission(
remote_endpoint_id,
stable_id,
"stream-revoke",
"presentation-revoke",
token,
token_payload.expect("token payload"),
"local-device",
crate::native_protocol::TokenStreamContract::OneShotAdmission,
)
.await
.expect("revoke transcript must prepare");
client.revoke_session_token(inbound_token);
assert!(
client
.confirm_reciprocal_admission(
remote_endpoint_id,
stable_id,
"stream-revoke",
"presentation-revoke",
true,
Some("user-device"),
)
.await
.is_err(),
"token revocation must retire pending reciprocal transcripts"
);
client.forget_session_connection(&connection_id);
client.register_session_token(
"transient-inbound-token".to_string(),
"drive-grant:transient".to_string(),
0,
);
client
.validate_session_token_for_connection("transient-inbound-token", &connection_id)
.await
.expect("transient inbound scope");
assert!(
client
.prepare_reciprocal_admission(
remote_endpoint_id,
stable_id,
"stream-transient",
"presentation-transient",
token,
token_payload.expect("token payload"),
"local-device",
crate::native_protocol::TokenStreamContract::OneShotAdmission,
)
.await
.is_err(),
"a transient admission must never disclose the cached user-device reciprocal credential"
);
}
#[tokio::test]
async fn user_device_admission_waits_for_late_authoritative_directory_binding() {
let dialer = Client::new_with_app_tag(
crate::test_constants::TEST_PROJECT_ID.to_string(),
"late-binding-dialer".to_string(),
Box::new(|| None),
);
let host = Client::new_with_app_tag(
crate::test_constants::TEST_PROJECT_ID.to_string(),
"late-binding-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;
let token = "late-binding-user-device-token";
host.register_session_token(token.to_string(), "user-device".to_string(), 0);
let compound = crate::session_token::build_ticket("host-ticket", token, "user-device", 0);
let (_, token_payload) = crate::session_token::split_ticket(&compound);
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 first_error = tokio::time::timeout(
Duration::from_secs(10),
dialer.present_and_accept_session_token_with_local_claim(
host_endpoint_id,
&connection_id,
token,
token_payload,
Some("late-binding-host-device".to_string()),
Some("late-binding-dialer-device".to_string()),
),
)
.await
.expect("initial presentation timeout")
.expect_err("directory binding has not reached the host yet");
assert_eq!(
first_error, "authoritative-device-binding-pending",
"missing directory evidence is a retryable ordering race, not an identity mismatch"
);
assert_eq!(
session_token_presentation_failure_action(&first_error),
SessionTokenPresentationFailureAction::RetryOnNextTransport,
);
assert!(
!host.inbound_session_token_admitted_for_transport(
&connection_id,
host.get_connection(dialer_endpoint_id)
.await
.expect("host transport")
.openrtc_generation(),
),
"pending identity evidence must not expose product traffic"
);
host.observe_authoritative_device_node(
"late-binding-dialer-device",
&dialer_endpoint_id.to_string(),
);
let scope = tokio::time::timeout(
Duration::from_secs(10),
dialer.present_and_accept_session_token_with_local_claim(
host_endpoint_id,
&connection_id,
token,
token_payload,
Some("late-binding-host-device".to_string()),
Some("late-binding-dialer-device".to_string()),
),
)
.await
.expect("retry presentation timeout")
.expect("the same token and transport should converge after directory observation");
assert_eq!(scope, "user-device");
let host_admission = tokio::time::timeout(Duration::from_secs(2), async {
loop {
let admission = host.session_admission(&connection_id);
if matches!(
&admission,
crate::session_token::SessionAdmission::Accepted {
scope: Some(scope),
authoritative_device_id: Some(device_id),
..
} if scope.as_str() == "user-device"
&& device_id == "late-binding-dialer-device"
) {
break admission;
}
tokio::time::sleep(Duration::from_millis(10)).await;
}
})
.await
.expect("host should commit the acknowledged identity");
assert!(
matches!(
&host_admission,
crate::session_token::SessionAdmission::Accepted {
scope: Some(scope),
authoritative_device_id: Some(device_id),
..
} if scope.as_str() == "user-device"
&& device_id == "late-binding-dialer-device"
),
"unexpected host admission after directory convergence: {host_admission:?}"
);
let host_record = host
.connection_manager
.get_by_connection_id(&connection_id)
.await
.expect("host manager row after acknowledged identity");
assert_eq!(host_record.state, ConnectionState::Connected);
assert_eq!(
host_record.device_id.as_deref(),
Some("late-binding-dialer-device")
);
assert!(host_record.transport_stable_id.is_some());
}
#[tokio::test]
async fn user_device_admission_rejects_a_proven_authoritative_device_mismatch() {
let dialer = Client::new_with_app_tag(
crate::test_constants::TEST_PROJECT_ID.to_string(),
"mismatch-dialer".to_string(),
Box::new(|| None),
);
let host = Client::new_with_app_tag(
crate::test_constants::TEST_PROJECT_ID.to_string(),
"mismatch-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;
host.observe_authoritative_device_node(
"actual-dialer-device",
&dialer_endpoint_id.to_string(),
);
let token = "mismatched-user-device-token";
host.register_session_token(token.to_string(), "user-device".to_string(), 0);
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 error = tokio::time::timeout(
Duration::from_secs(10),
dialer.present_and_accept_session_token_with_local_claim(
host_endpoint_id,
&connection_id,
token,
None,
Some("mismatch-host-device".to_string()),
Some("forged-dialer-device".to_string()),
),
)
.await
.expect("mismatched presentation timeout")
.expect_err("a conflicting authoritative device claim must be rejected");
assert!(
error.starts_with("[session-token-response:rejected] authoritative-device-mismatch:")
);
assert_eq!(
session_token_presentation_failure_action(&error),
SessionTokenPresentationFailureAction::RejectAndDisconnect,
);
assert!(matches!(
host.session_admission(&connection_id),
crate::session_token::SessionAdmission::Rejected { reason }
if reason.starts_with("authoritative-device-mismatch:")
));
}
#[tokio::test]
async fn native_peers_complete_admission_and_two_way_media() {
let dialer = std::sync::Arc::new(Client::new_with_app_tag(
crate::test_constants::TEST_PROJECT_ID.to_string(),
"inline-native-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(),
"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_ticket("host-ticket", host_token, "user-device", 0);
let dialer_compound =
crate::session_token::build_ticket("dialer-ticket", dialer_token, "user-device", 0);
let (_, host_payload) = crate::session_token::split_ticket(&host_compound);
let (_, dialer_payload) = crate::session_token::split_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")
.openrtc_generation();
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()),
Some("dialer-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")
.openrtc_generation();
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,)
);
exchange_native_media_publication(
dialer.clone(),
host.clone(),
"host-device",
"dialer-device",
crate::media::MediaKind::Audio,
crate::media::MediaCodec::Opus,
1,
)
.await;
exchange_native_media_publication(
dialer.clone(),
host.clone(),
"host-device",
"dialer-device",
crate::media::MediaKind::Video,
crate::media::MediaCodec::Vp8,
2,
)
.await;
exchange_native_media_publication(
host.clone(),
dialer.clone(),
"dialer-device",
"host-device",
crate::media::MediaKind::Audio,
crate::media::MediaCodec::Opus,
3,
)
.await;
exchange_native_media_publication(
host.clone(),
dialer.clone(),
"dialer-device",
"host-device",
crate::media::MediaKind::Video,
crate::media::MediaCodec::Vp8,
4,
)
.await;
}
#[tokio::test]
async fn manual_disconnect_reconnect_reestablishes_bilateral_native_admission() {
let dialer = std::sync::Arc::new(Client::new_with_app_tag(
crate::test_constants::TEST_PROJECT_ID.to_string(),
"manual-reconnect-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(),
"manual-reconnect-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();
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 = "manual-reconnect-host-token";
let dialer_token = "manual-reconnect-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_endpoint_ticket = host.endpoint_ticket().await.expect("host endpoint ticket");
let host_ticket = crate::session_token::build_ticket(
host_endpoint_ticket.as_str(),
host_token,
"user-device",
0,
);
let dialer_endpoint_ticket = dialer
.endpoint_ticket()
.await
.expect("dialer endpoint ticket");
let dialer_ticket = crate::session_token::build_ticket(
dialer_endpoint_ticket.as_str(),
dialer_token,
"user-device",
0,
);
let (_, dialer_payload) = crate::session_token::split_ticket(&dialer_ticket);
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(),
},
);
let initial = tokio::time::timeout(
Duration::from_secs(10),
dialer.connect_device(Some("host-device"), &host_ticket),
)
.await
.expect("initial connect timeout")
.expect("initial connect");
let connection_id = initial.connection_id;
let initial_stable_id = dialer
.get_connection(host_endpoint_id)
.await
.expect("initial dialer transport")
.openrtc_generation();
assert!(
dialer.remote_session_token_admitted_for_transport(&connection_id, initial_stable_id,),
"initial outbound proof must settle",
);
assert!(
dialer.inbound_session_token_admitted_for_transport(&connection_id, initial_stable_id,),
"initial reciprocal proof must settle",
);
let retired = dialer
.disconnect_device("host-device", Some(&host_endpoint_id.to_string()))
.await;
assert_eq!(retired, vec![connection_id.clone()]);
tokio::time::timeout(Duration::from_secs(3), async {
while dialer.get_connection(host_endpoint_id).await.is_some()
|| host.get_connection(dialer_endpoint_id).await.is_some()
{
tokio::time::sleep(Duration::from_millis(10)).await;
}
})
.await
.expect("manual disconnect must close both physical legs");
let reconnected = tokio::time::timeout(
Duration::from_secs(10),
dialer.connect_device(Some("host-device"), &host_ticket),
)
.await
.expect("explicit reconnect timeout")
.expect("explicit reconnect");
assert_eq!(reconnected.connection_id, connection_id);
let replacement_stable_id = dialer
.get_connection(host_endpoint_id)
.await
.expect("replacement dialer transport")
.openrtc_generation();
assert_ne!(
replacement_stable_id, initial_stable_id,
"explicit reconnect must install a fresh physical generation",
);
assert!(
dialer.remote_session_token_admitted_for_transport(
&connection_id,
replacement_stable_id,
),
"replacement outbound proof must settle",
);
assert!(
dialer.inbound_session_token_admitted_for_transport(
&connection_id,
replacement_stable_id,
),
"replacement reciprocal proof must settle",
);
assert!(
!host.is_auto_connect_peer_excluded(
"dialer-device",
Some(&dialer_endpoint_id.to_string()),
),
"an authenticated explicit reconnect must clear the peer-requested exclusion",
);
}
#[tokio::test]
async fn accepted_user_device_presentation_without_reciprocal_flag_requests_reverse_proof() {
crate::ensure_rustls();
let dialer = Client::new_with_app_tag(
crate::test_constants::TEST_PROJECT_ID.to_string(),
"replacement-proof-dialer".to_string(),
Box::new(|| None),
);
let host = Client::new_with_app_tag(
crate::test_constants::TEST_PROJECT_ID.to_string(),
"replacement-proof-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()));
host.observe_authoritative_device_node("dialer-device", &dialer_endpoint_id.to_string());
let token = "replacement-proof-host-token";
host.register_session_token(token.to_string(), "user-device".to_string(), 0);
let compound = crate::session_token::build_ticket("host-ticket", token, "user-device", 0);
let (_, token_payload) = crate::session_token::split_ticket(&compound);
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 scope = tokio::time::timeout(
Duration::from_secs(10),
dialer.present_token_to_host(
host_endpoint_id,
&connection_id,
token,
token_payload,
Some("dialer-device"),
),
)
.await
.expect("presentation timeout")
.expect("accepted presentation");
assert_eq!(scope, "user-device");
let host_stable_id = host
.get_connection(dialer_endpoint_id)
.await
.expect("host transport")
.openrtc_generation();
tokio::time::timeout(Duration::from_secs(2), async {
while !host.inbound_session_token_admitted_for_transport(&connection_id, host_stable_id)
|| !host.has_pending_reciprocal_session_admission_request(&connection_id)
{
tokio::time::sleep(Duration::from_millis(10)).await;
}
})
.await
.expect("host reciprocal request must follow the accepted inbound proof");
assert!(
host.inbound_session_token_admitted_for_transport(&connection_id, host_stable_id),
"the host must bind the accepted inbound proof to this physical generation",
);
assert!(
!host.remote_session_token_admitted_for_transport(&connection_id, host_stable_id),
"the test must begin with no current reverse-direction proof",
);
assert!(
host.has_pending_reciprocal_session_admission_request(&connection_id),
"a fresh user-device proof is typed input for the sole desired-peer actor even when the wire flag was consumed on a prior generation",
);
}
#[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
.auto_connect_loop_key
.lock()
.expect("dialer identity") = Some((
"user-1".to_string(),
"existing-live-dialer-device".to_string(),
));
*host.auto_connect_loop_key.lock().expect("host identity") = Some((
"user-1".to_string(),
"existing-live-host-device".to_string(),
));
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_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_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,
);
host.observe_authoritative_device_node(
"existing-live-dialer-device",
&dialer_endpoint_id.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();
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(),
"existing-live-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(),
"existing-live-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_connection_admitted(&connection_id));
assert!(matches!(
dialer.session_admission(&connection_id),
crate::session_token::SessionAdmission::Accepted {
mechanism: crate::session_token::AdmissionMechanism::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());
let settlement = 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;
if settlement.is_err() {
let dialer_stable = dialer
.connection_manager
.get_by_connection_id(&connection_id)
.await
.and_then(|record| record.transport_stable_id);
let host_stable = host
.connection_manager
.get_by_connection_id(&connection_id)
.await
.and_then(|record| record.transport_stable_id);
eprintln!(
"settlement timeout dialer={:?} host={:?} dialer_stable={:?} dialer_confirmed={} host_stable={:?} host_confirmed={}",
dialer.session_admission_block_reason(&connection_id),
host.session_admission_block_reason(&connection_id),
dialer_stable,
dialer.connection_application_crypto_is_confirmed(&connection_id, dialer_stable),
host_stable,
host.connection_application_crypto_is_confirmed(&connection_id, host_stable),
);
}
settlement.expect("bilateral admission and application crypto should settle");
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")
.openrtc_generation();
let host_transport_stable_id = host
.get_connection(dialer_endpoint_id)
.await
.expect("host live transport before proof invalidation")
.openrtc_generation();
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("existing-live-dialer-device"),
)
.with_session_token_stream_contract(
crate::native_protocol::TokenStreamContract::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(),
"existing-live-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,
"existing-live-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);
let idempotent = dialer
.connect_device(Some(remote_device_id), compound_ticket.as_str())
.await
.expect("idempotent managed connect preserves current admission");
assert_eq!(
idempotent.approved_scope.as_deref(),
Some("user-device"),
"an already-admitted physical generation must return the host-authoritative scope",
);
tokio::time::timeout(
Duration::from_secs(2),
dialer.try_auto_connect_device(
"user-1",
device.clone(),
"existing-live-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_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,
"existing-live-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;
}
#[test]
fn external_coordination_owns_excluded_peer_publication() {
assert!(
Client::should_publish_excluded_peers_to_signaling(false),
"the Firebase signaling adapter remains the publication owner in the legacy lane",
);
assert!(
!Client::should_publish_excluded_peers_to_signaling(true),
"an external coordination provider must prevent duplicate RTDB publication",
);
}
#[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_without_retiring_current_session() {
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());
client
.connection_manager
.upsert_pending(
"conn-explicit-session-survives-exclusion".to_string(),
Some(remote_node_id_str.clone()),
Some(remote_device_id.to_string()),
Some(remote_node_id_str.clone()),
)
.await;
client
.connection_manager
.set_connected(
"conn-explicit-session-survives-exclusion",
Some(remote_node_id_str.clone()),
)
.await;
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"
);
let retained = client
.connection_manager
.get_by_connection_id("conn-explicit-session-survives-exclusion")
.await
.expect("roster auto-connect policy must retain the admitted session");
assert_eq!(retained.state, ConnectionState::Connected);
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_known_device_with_token_uses_rust_owned_endpoint_state() {
let dialer = Client::new_with_app_tag(
crate::test_constants::TEST_PROJECT_ID.to_string(),
"known-admission-dialer".to_string(),
Box::new(|| None),
);
let host = Client::new_with_app_tag(
crate::test_constants::TEST_PROJECT_ID.to_string(),
"known-admission-host".to_string(),
Box::new(|| None),
);
let endpoint_dialer = 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 endpoint_host = 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(endpoint_dialer).await;
host.adopt_endpoint(endpoint_host).await;
let host_endpoint = host.get_endpoint().await.expect("host endpoint");
let host_addr = wait_for_endpoint_addr(&host_endpoint, 5_000)
.await
.expect("host endpoint addr");
let host_endpoint_id = host_addr.id;
host.register_session_token(
"known-device-token".to_string(),
"drive-grant:known".to_string(),
1,
);
let dialer_task = dialer.clone();
let connect = tokio::spawn(async move {
dialer_task
.connect_known_device_with_token(
"host-device",
"known-device-token",
"drive-grant:known",
1,
Some(crate::session_token::now_unix_ms() + 60_000),
Some(5_000),
)
.await
});
tokio::time::sleep(Duration::from_millis(50)).await;
assert!(
!connect.is_finished(),
"Rust should await its own late endpoint state instead of failing immediately"
);
let presence_ticket = crate::session_token::build_ticket(
&iroh_tickets::endpoint::EndpointTicket::new(host_addr.clone()).to_string(),
"presence-observation-token",
"friend",
0,
);
dialer
.observe_known_device_endpoint("host-device", &presence_ticket)
.await
.expect("provider-authorized compound presence ticket should be observed");
let result = tokio::time::timeout(Duration::from_secs(10), connect)
.await
.expect("known-device admission timeout")
.expect("known-device admission task")
.expect("known-device admission should connect");
assert_eq!(result.device_id.as_deref(), Some("host-device"));
assert_eq!(result.approved_scope.as_deref(), Some("drive-grant:known"));
let settled = dialer
.wait_for_settled_scope("drive-grant:known", Some(5_000))
.await
.expect("known-device product scope should settle in Rust");
assert!(settled.settled_ready);
assert!(settled
.scopes
.iter()
.any(|scope| scope == "drive-grant:known"));
assert!(!settled.scopes.iter().any(|scope| scope == "user-device"));
let initial_transport_stable_id = dialer
.get_connection(host_endpoint_id)
.await
.expect("initial scoped transport")
.openrtc_generation();
dialer
.get_connection(host_endpoint_id)
.await
.expect("initial scoped transport before simulated outage")
.close(0u32.into(), b"simulated-network-change");
let close_deadline = tokio::time::Instant::now() + Duration::from_secs(5);
while dialer.get_connection(host_endpoint_id).await.is_some() {
assert!(
tokio::time::Instant::now() < close_deadline,
"simulated outage did not retire the old physical generation"
);
tokio::time::sleep(Duration::from_millis(25)).await;
}
dialer
.ensure_connected_addr(host_endpoint_id, host_addr)
.await
.expect("Rust transport owner should reconnect and restore scoped admission");
let recovered = dialer
.wait_for_settled_scope("drive-grant:known", Some(5_000))
.await
.expect("recovered product scope should settle without another app connect");
assert!(recovered.settled_ready);
assert!(recovered
.scopes
.iter()
.any(|scope| scope == "drive-grant:known"));
assert!(!recovered.scopes.iter().any(|scope| scope == "user-device"));
assert_ne!(
dialer
.get_connection(host_endpoint_id)
.await
.expect("recovered scoped transport")
.openrtc_generation(),
initial_transport_stable_id,
"recovery proof must belong to a new physical generation"
);
let recovered_transport_stable_id = dialer
.get_connection(host_endpoint_id)
.await
.expect("recovered scoped transport before grant replacement")
.openrtc_generation();
host.revoke_tokens_by_scope("drive-grant:known").await;
host.register_session_token(
"known-device-replacement-token".to_string(),
"drive-grant:replacement".to_string(),
1,
);
let replacement = tokio::time::timeout(
Duration::from_secs(20),
dialer.connect_known_device_with_token(
"host-device",
"known-device-replacement-token",
"drive-grant:replacement",
1,
Some(crate::session_token::now_unix_ms() + 60_000),
Some(5_000),
),
)
.await
.expect("replacement known-device admission timeout")
.expect("replacement known-device admission should connect");
assert_eq!(
replacement.approved_scope.as_deref(),
Some("drive-grant:replacement")
);
assert_ne!(
dialer
.get_connection(host_endpoint_id)
.await
.expect("replacement scoped transport")
.openrtc_generation(),
recovered_transport_stable_id,
"revoking and replacing a scoped grant must not reuse its retired physical generation"
);
}
#[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 session_token_presentation_redials_and_reopens_scoped_session_after_transport_drop() {
let dialer = Client::new_with_app_tag(
crate::test_constants::TEST_PROJECT_ID.to_string(),
"scoped-admission-redial-a".to_string(),
Box::new(|| None),
);
let host = Client::new_with_app_tag(
crate::test_constants::TEST_PROJECT_ID.to_string(),
"scoped-admission-redial-b".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");
dialer.adopt_endpoint(dialer_endpoint).await;
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 token = "scoped-admission-redial-token";
let scope = "drive:test";
host.register_session_token(token.to_string(), scope.to_string(), 0);
dialer
.ensure_connected_addr(host_endpoint_id, host_addr)
.await
.expect("initial transport");
let local_node_id = dialer.current_node_id().await.expect("dialer node id");
let connection_id =
Client::deterministic_connection_id(&local_node_id, &host_endpoint_id.to_string());
dialer
.present_and_accept_session_token_with_local_claim(
host_endpoint_id,
&connection_id,
token,
None,
None,
Some("dialer-device".to_string()),
)
.await
.expect("initial scoped admission");
let first_stable_id = dialer
.get_connection(host_endpoint_id)
.await
.expect("initial physical connection")
.openrtc_generation();
dialer
.disconnect_with_reason(
host_endpoint_id,
crate::lifecycle_reason::REASON_NETWORK_CHANGE_RECONNECT,
)
.await
.expect("drop physical transport");
dialer
.connection_manager
.set_closed(
&connection_id,
Some("incoming-transport-closed-without-live-replacement".to_string()),
)
.await;
let repaired_scope = tokio::time::timeout(
Duration::from_secs(10),
dialer.present_and_accept_session_token_with_local_claim(
host_endpoint_id,
&connection_id,
token,
None,
None,
Some("dialer-device".to_string()),
),
)
.await
.expect("scoped admission repair timeout")
.expect("scoped admission repair");
assert_eq!(repaired_scope, scope);
let repaired = dialer
.connection_manager
.get_by_connection_id(&connection_id)
.await
.expect("repaired logical connection");
assert_eq!(repaired.state, ConnectionState::Connected);
assert!(
repaired
.transport_stable_id
.is_some_and(|stable_id| stable_id != first_stable_id),
"repair must publish the replacement physical generation"
);
}
#[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.openrtc_generation())
.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 native_main_ack_binds_the_exact_control_stream_generation_after_projection_repair() {
let client_a = Client::new_with_app_tag(
crate::test_constants::TEST_PROJECT_ID.to_string(),
"native-main-owner-fence-a".to_string(),
Box::new(|| None),
);
let client_b = Client::new_with_app_tag(
crate::test_constants::TEST_PROJECT_ID.to_string(),
"native-main-owner-fence-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_id = client_b.get_endpoint().await.expect("endpoint b").id();
let ticket_b = client_b.endpoint_ticket().await.expect("endpoint ticket");
let connection = client_a
.connect_device(Some("device-b"), ticket_b.as_str())
.await
.expect("managed connect succeeds");
client_a
.connection_manager
.mark_transport_replaced(
&connection.connection_id,
None,
Some("stale-browser-projection".to_string()),
Some(crate::lifecycle_reason::REASON_REPLACEMENT_IN_PROGRESS.to_string()),
)
.await
.expect("install stale projection");
assert_eq!(
client_a
.connection_manager
.get_by_connection_id(&connection.connection_id)
.await
.expect("stale manager record")
.transport_stable_id,
None,
"the regression must begin before the key waiter freezes its transcript generation",
);
let missing_generation_error = match client_a
.accept_application_key_handshake(
&connection.connection_id,
&connection.remote_node_id,
None,
Some("ack"),
[7_u8; crate::key_agreement::PUBLIC_KEY_BYTES],
)
.await
{
Ok(_) => panic!("an ACK without Rust stream provenance must fail closed"),
Err(error) => error,
};
assert!(
missing_generation_error
.to_string()
.contains("missing the Rust-owned transport stable id"),
"unexpected missing-generation error: {missing_generation_error:#}",
);
assert!(
!client_a
.connection_application_crypto_has_bound_confirmation(&connection.connection_id,),
"a provenance-free ACK must not create a confirmation binding",
);
client_a
.ensure_connection_manager_record_before_peer_stream(&endpoint_b_id)
.await
.expect("control opener repairs physical ownership");
let node = client_a
.iroh_node
.read()
.await
.as_ref()
.cloned()
.expect("native node");
let (opener_transport_stable_id, _send, _recv) = node
.open_bi_with_transport_stable_id(endpoint_b_id)
.await
.expect("native-main control opener");
let frozen_key_waiter_stable_id = client_a
.connection_manager
.get_by_connection_id(&connection.connection_id)
.await
.expect("key waiter generation")
.transport_stable_id;
assert_eq!(
frozen_key_waiter_stable_id,
Some(opener_transport_stable_id),
"the key waiter must freeze the same generation returned by the control opener",
);
let fence = client_a
.incoming_stream_generation_fence(endpoint_b_id, opener_transport_stable_id)
.await;
assert_eq!(
(
fence.incoming_transport_stable_id,
fence.physical_transport_stable_id,
fence.managed_transport_stable_id,
fence.owned,
),
(
opener_transport_stable_id,
Some(opener_transport_stable_id),
Some(opener_transport_stable_id),
true,
),
"Rust opener metadata, physical registry, and final manager fence must identify one exact generation",
);
client_a
.accept_application_key_handshake(
&connection.connection_id,
&connection.remote_node_id,
Some(opener_transport_stable_id),
Some("ack"),
[7_u8; crate::key_agreement::PUBLIC_KEY_BYTES],
)
.await
.expect("exact ACK passes the Rust owner fence");
assert!(client_a.connection_application_crypto_is_confirmed(
&connection.connection_id,
Some(opener_transport_stable_id),
));
}
#[tokio::test]
async fn physical_observation_cannot_reopen_terminal_capability() {
let client = Client::new_with_app_tag(
"local-test".into(),
"observation-a".into(),
Box::new(|| None),
);
let peer = Client::new_with_app_tag(
"local-test".into(),
"observation-b".into(),
Box::new(|| None),
);
for host in [&client, &peer] {
let endpoint = Endpoint::builder(iroh::endpoint::presets::Minimal)
.alpns(vec![crate::native_node::PlutoniumProtocol::ALPN.to_vec()])
.relay_mode(RelayMode::Disabled)
.bind_addr("127.0.0.1:0".parse::<std::net::SocketAddr>().unwrap())
.unwrap()
.bind()
.await
.unwrap();
host.adopt_endpoint(endpoint).await;
}
let connected = client
.connect_device(None, &peer.endpoint_ticket().await.unwrap())
.await
.unwrap();
let stable = client
.get_connection(connected.remote_node_id.parse().unwrap())
.await
.unwrap()
.openrtc_generation();
for reason in [
crate::lifecycle_reason::REASON_DESIRED_PEER_WITHDRAWN,
crate::lifecycle_reason::REASON_SESSION_TOKEN_REVOKED,
crate::lifecycle_reason::REASON_SESSION_ADMISSION_REJECTED,
] {
client
.connection_manager
.set_closed(&connected.connection_id, Some(reason.into()))
.await
.unwrap();
assert!(
!client
.commit_current_native_transport_record(
&connected.connection_id,
&connected.remote_node_id,
None,
Some(stable),
Some("generic-physical-observation".into())
)
.await,
"physical observation is not fresh capability authority: {reason}"
);
assert_eq!(
client
.connection_manager
.get_by_connection_id(&connected.connection_id)
.await
.unwrap()
.state,
crate::connection_manager::ConnectionState::Closed
);
}
}
#[tokio::test]
async fn delayed_transport_commit_and_key_ack_cannot_regress_the_current_generation() {
let client_a = Client::new_with_app_tag(
crate::test_constants::TEST_PROJECT_ID.to_string(),
"generation-cas-a".to_string(),
Box::new(|| None),
);
let client_b = Client::new_with_app_tag(
crate::test_constants::TEST_PROJECT_ID.to_string(),
"generation-cas-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 remote_endpoint = client_b.get_endpoint().await.expect("endpoint b").id();
let ticket_b = client_b.endpoint_ticket().await.expect("endpoint ticket");
let connection = client_a
.connect_device(Some("device-b"), ticket_b.as_str())
.await
.expect("initial connect");
let node_a = client_a
.iroh_node
.read()
.await
.as_ref()
.cloned()
.expect("node a");
let node_b = client_b
.iroh_node
.read()
.await
.as_ref()
.cloned()
.expect("node b");
let remote_addr = node_b.node_addr().await.expect("remote addr");
let stale_a = node_a
.get_connection(remote_endpoint)
.await
.map(|connection| connection.openrtc_generation())
.expect("generation a");
let (release_stale_tx, release_stale_rx) = oneshot::channel();
let stale_client = client_a.clone();
let stale_connection_id = connection.connection_id.clone();
let stale_remote_node_id = connection.remote_node_id.clone();
let stale_commit = tokio::spawn(async move {
release_stale_rx.await.expect("release stale commit");
stale_client
.commit_current_native_transport_record(
&stale_connection_id,
&stale_remote_node_id,
Some("device-b".to_string()),
Some(stale_a),
Some("delayed-generation-a".to_string()),
)
.await;
});
node_a
.disconnect_with_reason(remote_endpoint, "test-replace-generation-a")
.await
.expect("disconnect generation a");
assert!(
node_a.get_connection(remote_endpoint).await.is_none(),
"the delayed caller must snapshot a real physical absence",
);
let (none_latched_tx, none_latched_rx) = oneshot::channel();
let (release_none_tx, release_none_rx) = oneshot::channel();
let none_client = client_a.clone();
let none_connection_id = connection.connection_id.clone();
let none_remote_node_id = connection.remote_node_id.clone();
let delayed_none_commit = tokio::spawn(async move {
none_latched_tx
.send(())
.expect("missing transport observation latched");
release_none_rx
.await
.expect("release missing transport observation");
none_client
.commit_current_native_transport_record(
&none_connection_id,
&none_remote_node_id,
Some("device-b".to_string()),
None,
Some("delayed-no-physical-observation".to_string()),
)
.await;
});
none_latched_rx
.await
.expect("missing transport observation blocks before B installs");
let mut reconnect = node_a.connect_addr(remote_endpoint, remote_addr);
let connected = tokio::time::timeout(Duration::from_secs(5), async {
while let Some(event) = futures::StreamExt::next(&mut reconnect).await {
match event {
crate::native_node::ConnectEvent::Connected => return true,
crate::native_node::ConnectEvent::Closed { .. } => return false,
}
}
false
})
.await
.expect("replacement connect timeout");
assert!(connected, "replacement must install generation b");
let current_b = node_a
.get_connection(remote_endpoint)
.await
.map(|connection| connection.openrtc_generation())
.expect("generation b");
assert_ne!(stale_a, current_b);
client_a
.connection_manager
.set_connected_with_transport(
&connection.connection_id,
Some(connection.remote_node_id.clone()),
Some(current_b),
Some("test-reconnect-input".to_string()),
)
.await
.expect("logical generation b input");
client_a
.commit_current_native_transport_record(
&connection.connection_id,
&connection.remote_node_id,
Some("device-b".to_string()),
Some(current_b),
Some("physical-generation-b".to_string()),
)
.await;
client_a
.accept_application_key_handshake(
&connection.connection_id,
&connection.remote_node_id,
Some(current_b),
Some("ack"),
[17_u8; crate::key_agreement::PUBLIC_KEY_BYTES],
)
.await
.expect("generation b key ACK");
release_none_tx
.send(())
.expect("release missing observation after B confirmation");
delayed_none_commit.await.expect("missing observation task");
assert!(
client_a.connection_application_crypto_is_confirmed(
&connection.connection_id,
Some(current_b),
),
"a delayed None observation must not unconfirm generation B",
);
let protected = client_a
.protect_outbound_application_payload(&connection.connection_id, b"generation-b")
.expect("advance generation b sequence");
assert_ne!(protected, b"generation-b");
let key_b = client_a
.connection_application_crypto_key(&connection.connection_id)
.expect("generation b key");
let agreement_b = client_a
.connection_key_agreement_public_key(&connection.connection_id)
.expect("generation b key agreement");
let sequence_b = client_a
.connection_application_crypto_outbound_sequences
.read()
.expect("sequence registry")
.get(&connection.connection_id)
.copied();
release_stale_tx.send(()).expect("release stale A commit");
stale_commit.await.expect("stale commit task");
let delayed_ack = client_a
.accept_application_key_handshake(
&connection.connection_id,
&connection.remote_node_id,
Some(stale_a),
Some("ack"),
[29_u8; crate::key_agreement::PUBLIC_KEY_BYTES],
)
.await;
assert!(delayed_ack.is_err(), "generation A ACK must fail closed");
let settled = client_a
.connection_manager
.get_by_connection_id(&connection.connection_id)
.await
.expect("settled manager record");
assert_eq!(settled.transport_stable_id, Some(current_b));
assert_eq!(
client_a.connection_application_crypto_key(&connection.connection_id),
Some(key_b),
"a delayed A ACK must not replace B's key",
);
assert_eq!(
client_a.connection_key_agreement_public_key(&connection.connection_id),
Some(agreement_b),
"a delayed A ACK must not replace B's key-agreement state",
);
assert!(client_a.connection_application_crypto_is_confirmed(
&connection.connection_id,
Some(current_b),
));
assert_eq!(
client_a
.connection_application_crypto_outbound_sequences
.read()
.expect("sequence registry")
.get(&connection.connection_id)
.copied(),
sequence_b,
"a delayed A ACK must not reset B's outbound sequence",
);
assert!(
!client_a
.incoming_stream_generation_fence(remote_endpoint, stale_a)
.await
.owned,
);
assert!(
client_a
.incoming_stream_generation_fence(remote_endpoint, current_b)
.await
.owned,
);
client_a.clear_connection_application_crypto_key(&connection.connection_id);
assert_eq!(
client_a.connection_key_agreement_public_key(&connection.connection_id),
None,
);
assert!(
client_a
.accept_application_key_handshake(
&connection.connection_id,
&connection.remote_node_id,
Some(stale_a),
Some("ack"),
[31_u8; crate::key_agreement::PUBLIC_KEY_BYTES],
)
.await
.is_err(),
"a stale ACK after cleanup must still fail closed",
);
assert_eq!(
client_a.connection_key_agreement_public_key(&connection.connection_id),
None,
"a rejected ACK must not recreate agreement state after cleanup",
);
}
#[tokio::test]
async fn connect_device_rebind_retires_previous_transport_crypto_confirmation() {
let client_a = Client::new_with_app_tag(
crate::test_constants::TEST_PROJECT_ID.to_string(),
"connect-rebind-crypto-a".to_string(),
Box::new(|| None),
);
let client_b = Client::new_with_app_tag(
crate::test_constants::TEST_PROJECT_ID.to_string(),
"connect-rebind-crypto-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.openrtc_generation())
.expect("live transport stable id");
let stale_stable_id = live_stable_id.saturating_add(100);
client_a
.connection_manager
.set_connected_with_transport(
&first.connection_id,
Some(first.remote_node_id.clone()),
Some(stale_stable_id),
Some("test-stale-physical-generation".to_string()),
)
.await;
client_a.bind_inbound_native_admission_stream_contract(
&first.connection_id,
crate::native_protocol::TokenStreamContract::PersistentControl,
);
client_a.bind_inbound_session_token_transport(&first.connection_id, stale_stable_id);
client_a.set_connection_application_crypto_required(&first.connection_id);
let _ = client_a
.get_or_create_connection_key_agreement(&first.connection_id)
.expect("application key agreement");
client_a.set_connection_application_crypto_key(&first.connection_id, [47u8; 32]);
client_a
.bind_connection_application_crypto_transport(&first.connection_id, stale_stable_id);
client_a.confirm_connection_application_crypto(&first.connection_id, stale_stable_id);
assert!(
client_a.connection_application_crypto_is_confirmed(
&first.connection_id,
Some(stale_stable_id),
),
"test setup must begin with the previous physical generation confirmed",
);
let rebound = client_a
.connect_device(Some("device-b"), ticket_b.as_str())
.await
.expect("idempotent connect reconciles the live replacement");
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.transport_stable_id, Some(live_stable_id));
assert!(
!client_a.connection_application_crypto_is_confirmed(
&first.connection_id,
Some(live_stable_id),
),
"a replacement transport must not inherit the preceding physical leg's key confirmation",
);
assert!(
!client_a.inbound_session_token_admitted_for_transport(
&first.connection_id,
stale_stable_id,
),
"the replacement commit must retire the preceding transport's admission proof",
);
let session = client_a
.peer_session("device-b")
.await
.expect("reconciled peer session");
assert!(
!session.settled_ready,
"product traffic must stay closed until the replacement key transcript confirms",
);
assert_eq!(
session.readiness_reason,
"application-crypto-confirmation-pending",
);
}
#[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");
}
#[test]
fn dial_timeout_reason_is_structured_as_transient_auto_connect_failure() {
let reason = crate::client::core_impl::dial_timeout_reason("ensure_connected_addr");
assert_eq!(reason, "auto-connect-failure:ensure_connected_addr-timeout");
assert!(crate::lifecycle_reason::is_transient(Some(&reason)));
assert!(!crate::lifecycle_reason::is_terminal(Some(&reason)));
}
#[cfg(all(not(target_arch = "wasm32"), feature = "transport-lan"))]
#[tokio::test]
async fn offline_admission_handoff_uses_current_rust_transport_owner() {
use crate::offline::{
OfflineAssurance, OfflineConnectionProof, OfflineDeviceCredential,
OfflineEnrollmentRequest, OfflineProofTranscript, OfflineReplayCache, OfflineSigner,
OfflineTransportBinding, OfflineTrustBundle, OfflineTrustState, SoftwareOfflineSigner,
OFFLINE_PROOF_PROTOCOL,
};
let config = TransportConfig {
network_policy: super::super::NetworkPolicy::LocalOnly,
..TransportConfig::default()
};
let client_a = super::super::ClientBuilder::new_with_app_tag(
crate::test_constants::TEST_PROJECT_ID.to_string(),
"offline-admission-a".to_string(),
Box::new(|| None),
)
.transport_config(config.clone())
.build();
let client_b = super::super::ClientBuilder::new_with_app_tag(
crate::test_constants::TEST_PROJECT_ID.to_string(),
"offline-admission-b".to_string(),
Box::new(|| None),
)
.transport_config(config)
.build();
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>().unwrap())
.unwrap()
.bind()
.await
.unwrap();
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>().unwrap())
.unwrap()
.bind()
.await
.unwrap();
client_a.adopt_endpoint(endpoint_a).await;
client_b.adopt_endpoint(endpoint_b).await;
let endpoint_b = client_b.get_endpoint().await.unwrap();
let remote_id = endpoint_b.id();
let remote_addr = wait_for_endpoint_addr(&endpoint_b, 5_000).await.unwrap();
let local_id = client_a.current_node_id().await.unwrap();
let issuer = SoftwareOfflineSigner::generate().unwrap();
let device = SoftwareOfflineSigner::generate().unwrap();
let request = OfflineEnrollmentRequest::create(
&device,
"field-a",
"device-b",
&remote_id.to_string(),
"enroll-b",
vec!["sensor".to_string()],
OfflineAssurance::Software,
1_000,
)
.unwrap();
let credential = OfflineDeviceCredential::issue(
&issuer,
&request,
"serial-b",
1,
vec!["sensor".to_string()],
OfflineAssurance::Software,
1_000,
10_000,
)
.unwrap();
let bundle =
OfflineTrustBundle::issue(&issuer, "field-a", 1, None, None, vec![credential], vec![])
.unwrap();
let mut trust = OfflineTrustState::new("field-a", issuer.verifying_key().unwrap()).unwrap();
trust.apply(bundle, 2_000).unwrap();
let candidate = trust
.authorize_local_candidate("device-b", remote_addr.clone(), 2_000)
.unwrap();
let connection_id = client_a
.offline()
.register_candidate(candidate.clone())
.await
.unwrap();
client_a
.connection_manager
.upsert_pending(
connection_id.clone(),
Some(remote_id.to_string()),
Some("device-b".to_string()),
Some(remote_id.to_string()),
)
.await;
client_a
.connection_manager
.set_connecting(&connection_id)
.await;
client_a
.ensure_connected_addr(remote_id, remote_addr)
.await
.unwrap();
let stable_id = client_a
.get_connection(remote_id)
.await
.map(|connection| crate::transport_generation::for_connection(&connection))
.unwrap();
let transcript = OfflineProofTranscript {
protocol: OFFLINE_PROOF_PROTOCOL.to_string(),
trust_domain: "field-a".to_string(),
credential_serial: "serial-b".to_string(),
presenter_device_id: "device-b".to_string(),
presenter_endpoint_id: remote_id.to_string(),
verifier_device_id: "device-a".to_string(),
verifier_endpoint_id: local_id.clone(),
presenter_nonce: "presenter-nonce".to_string(),
verifier_nonce: "verifier-nonce".to_string(),
transport_stable_id: stable_id,
channel_binding: format!("test-generation-{stable_id}"),
};
let proof = OfflineConnectionProof::create(&device, transcript.clone()).unwrap();
let handoff = trust
.verify_transport_proof(
&candidate,
&proof,
&transcript,
OfflineTransportBinding {
local_endpoint_id: local_id,
remote_endpoint_id: remote_id.to_string(),
transport_stable_id: stable_id,
},
&mut OfflineReplayCache::new(8),
2_000,
)
.unwrap();
client_a.offline().commit_admission(handoff).await.unwrap();
assert!(matches!(
client_a.session_admission(&connection_id),
crate::session_token::SessionAdmission::Accepted {
mechanism: crate::session_token::AdmissionMechanism::OfflineDeviceProof,
authoritative_device_id: Some(device_id),
..
} if device_id == "device-b"
));
assert!(client_a.transport_is_admitted(&connection_id, stable_id));
assert!(
!client_a.native_application_stream_admitted_for_transport(&connection_id, stable_id)
);
client_a.set_connection_application_crypto_key(&connection_id, [29u8; 32]);
client_a.bind_connection_application_crypto_transport(&connection_id, stable_id);
client_a.confirm_connection_application_crypto(&connection_id, stable_id);
assert!(
client_a.native_application_stream_admitted_for_transport(&connection_id, stable_id)
);
assert!(
client_a.invalidate_native_main_route_proofs_for_transport(&connection_id, stable_id)
);
assert!(client_a.offline_admission_is_required(&connection_id));
assert!(!client_a.transport_is_admitted(&connection_id, stable_id));
client_a.clear_session_tokens();
assert!(client_a.offline_admission_is_required(&connection_id));
assert!(matches!(
client_a.session_admission(&connection_id),
crate::session_token::SessionAdmission::Accepted {
mechanism: crate::session_token::AdmissionMechanism::OfflineDeviceProof,
..
}
));
}
#[cfg(all(not(target_arch = "wasm32"), feature = "transport-lan"))]
#[tokio::test]
async fn two_native_local_only_candidates_complete_bilateral_offline_proof() {
use crate::offline::{
DurableOfflineTrustState, OfflineAssurance, OfflineDeviceCredential,
OfflineEnrollmentRequest, OfflineRuntimeConfig, OfflineSigner, OfflineTrustBundle,
SoftwareOfflineSigner,
};
let config = TransportConfig {
network_policy: super::super::NetworkPolicy::LocalOnly,
..TransportConfig::default()
};
let client_a = super::super::ClientBuilder::new_with_app_tag(
crate::test_constants::TEST_PROJECT_ID.to_string(),
"offline-live-a".to_string(),
Box::new(|| None),
)
.transport_config(config.clone())
.build();
let client_b = super::super::ClientBuilder::new_with_app_tag(
crate::test_constants::TEST_PROJECT_ID.to_string(),
"offline-live-b".to_string(),
Box::new(|| None),
)
.transport_config(config)
.build();
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>().unwrap())
.unwrap()
.bind()
.await
.unwrap();
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>().unwrap())
.unwrap()
.bind()
.await
.unwrap();
client_a.adopt_endpoint(endpoint_a).await;
client_b.adopt_endpoint(endpoint_b).await;
let endpoint_a = client_a.get_endpoint().await.unwrap();
let endpoint_b = client_b.get_endpoint().await.unwrap();
let addr_a = wait_for_endpoint_addr(&endpoint_a, 5_000).await.unwrap();
let addr_b = wait_for_endpoint_addr(&endpoint_b, 5_000).await.unwrap();
assert!(crate::local_discovery::endpoint_addr_is_local_only(
&addr_a,
&[]
));
assert!(crate::local_discovery::endpoint_addr_is_local_only(
&addr_b,
&[]
));
let issuer = SoftwareOfflineSigner::generate().unwrap();
let signer_a = std::sync::Arc::new(SoftwareOfflineSigner::generate().unwrap());
let signer_b = std::sync::Arc::new(SoftwareOfflineSigner::generate().unwrap());
let now = crate::coordination::now_millis_u64();
let request_a = OfflineEnrollmentRequest::create(
signer_a.as_ref(),
"field-live",
"device-a",
&endpoint_a.id().to_string(),
"enroll-a",
vec!["peer".to_string()],
OfflineAssurance::Software,
now,
)
.unwrap();
let request_b = OfflineEnrollmentRequest::create(
signer_b.as_ref(),
"field-live",
"device-b",
&endpoint_b.id().to_string(),
"enroll-b",
vec!["peer".to_string()],
OfflineAssurance::Software,
now,
)
.unwrap();
let credential_a = OfflineDeviceCredential::issue(
&issuer,
&request_a,
"serial-a",
1,
vec!["peer".to_string()],
OfflineAssurance::Software,
now,
now + 60_000,
)
.unwrap();
let credential_b = OfflineDeviceCredential::issue(
&issuer,
&request_b,
"serial-b",
1,
vec!["peer".to_string()],
OfflineAssurance::Software,
now,
now + 60_000,
)
.unwrap();
let bundle = OfflineTrustBundle::issue(
&issuer,
"field-live",
1,
None,
None,
vec![credential_a.clone(), credential_b.clone()],
vec![],
)
.unwrap();
let first_bundle_digest = bundle.digest().unwrap();
let state_dir = std::env::temp_dir().join(format!(
"openrtc-offline-live-{}",
crate::session_token::generate_nonce()
));
std::fs::create_dir_all(&state_dir).unwrap();
let mut trust_a = DurableOfflineTrustState::open(
state_dir.join("a.json"),
"field-live",
issuer.verifying_key().unwrap(),
[],
now,
)
.unwrap();
trust_a.apply(bundle.clone(), now).unwrap();
let mut trust_b = DurableOfflineTrustState::open(
state_dir.join("b.json"),
"field-live",
issuer.verifying_key().unwrap(),
[],
now,
)
.unwrap();
trust_b.apply(bundle.clone(), now).unwrap();
let trust_a_owner = std::sync::Arc::new(std::sync::Mutex::new(trust_a));
let trust_b_owner = std::sync::Arc::new(std::sync::Mutex::new(trust_b));
client_a
.offline()
.install_runtime(OfflineRuntimeConfig {
local_device_id: "device-a".to_string(),
signer: signer_a.clone(),
trust: trust_a_owner.clone(),
})
.await
.unwrap();
client_b
.offline()
.install_runtime(OfflineRuntimeConfig {
local_device_id: "device-b".to_string(),
signer: signer_b.clone(),
trust: trust_b_owner.clone(),
})
.await
.unwrap();
let (observed_a, observed_b) = tokio::join!(
client_a.observe_offline_lan_candidate(addr_b.clone()),
client_b.observe_offline_lan_candidate(addr_a.clone()),
);
assert!(observed_a.unwrap());
assert!(observed_b.unwrap());
let (revision_before_reinstall, candidates_before_reinstall) = {
let guard = client_a.offline_runtime.lock().await;
let runtime = guard.as_ref().unwrap();
(runtime.desired_revision, runtime.candidates.len())
};
client_a
.offline()
.install_runtime(OfflineRuntimeConfig {
local_device_id: "device-a".to_string(),
signer: signer_a.clone(),
trust: trust_a_owner.clone(),
})
.await
.expect("the exact installed owner must be idempotent");
let mut replacement_trust = DurableOfflineTrustState::open(
state_dir.join("replacement-a.json"),
"field-live",
issuer.verifying_key().unwrap(),
[],
now,
)
.unwrap();
replacement_trust.apply(bundle, now).unwrap();
let replacement_error = client_a
.offline()
.install_runtime(OfflineRuntimeConfig {
local_device_id: "device-a".to_string(),
signer: signer_a.clone(),
trust: std::sync::Arc::new(std::sync::Mutex::new(replacement_trust)),
})
.await
.expect_err("a different trust owner must require explicit replacement")
.to_string();
assert!(replacement_error.contains("already installed"));
{
let guard = client_a.offline_runtime.lock().await;
let runtime = guard.as_ref().unwrap();
assert!(std::sync::Arc::ptr_eq(&runtime.trust, &trust_a_owner));
assert_eq!(runtime.desired_revision, revision_before_reinstall);
assert_eq!(runtime.candidates.len(), candidates_before_reinstall);
}
let connection_id = Client::deterministic_connection_id(
&endpoint_a.id().to_string(),
&endpoint_b.id().to_string(),
);
let deadline = tokio::time::Instant::now() + std::time::Duration::from_secs(12);
loop {
let accepted_a = matches!(
client_a.session_admission(&connection_id),
crate::session_token::SessionAdmission::Accepted {
mechanism: crate::session_token::AdmissionMechanism::OfflineDeviceProof,
authoritative_device_id: Some(device_id),
..
} if device_id == "device-b"
);
let accepted_b = matches!(
client_b.session_admission(&connection_id),
crate::session_token::SessionAdmission::Accepted {
mechanism: crate::session_token::AdmissionMechanism::OfflineDeviceProof,
authoritative_device_id: Some(device_id),
..
} if device_id == "device-a"
);
let stable_a = client_a
.connection_manager
.get_by_connection_id(&connection_id)
.await
.and_then(|record| record.transport_stable_id);
let stable_b = client_b
.connection_manager
.get_by_connection_id(&connection_id)
.await
.and_then(|record| record.transport_stable_id);
let ready_a = stable_a.is_some_and(|stable_id| {
client_a.native_application_stream_admitted_for_transport(&connection_id, stable_id)
});
let ready_b = stable_b.is_some_and(|stable_id| {
client_b.native_application_stream_admitted_for_transport(&connection_id, stable_id)
});
if accepted_a && accepted_b && ready_a && ready_b {
break;
}
assert!(
tokio::time::Instant::now() < deadline,
"bilateral offline proof did not converge: a={:?} b={:?}",
client_a.session_admission(&connection_id),
client_b.session_admission(&connection_id),
);
tokio::time::sleep(std::time::Duration::from_millis(25)).await;
}
assert_eq!(client_a.connection_manager.list_active().await.len(), 1);
assert_eq!(client_b.connection_manager.list_active().await.len(), 1);
let stable_a = client_a
.connection_manager
.get_by_connection_id(&connection_id)
.await
.and_then(|record| record.transport_stable_id)
.unwrap();
let stable_b = client_b
.connection_manager
.get_by_connection_id(&connection_id)
.await
.and_then(|record| record.transport_stable_id)
.unwrap();
let (expired_a, expired_b) = tokio::join!(
client_a.expire_offline_lan_candidate(endpoint_b.id()),
client_b.expire_offline_lan_candidate(endpoint_a.id()),
);
assert!(expired_a.unwrap());
assert!(expired_b.unwrap());
tokio::time::sleep(std::time::Duration::from_millis(100)).await;
assert!(client_a.get_connection(endpoint_b.id()).await.is_some());
assert!(client_b.get_connection(endpoint_a.id()).await.is_some());
assert!(client_a.native_application_stream_admitted_for_transport(&connection_id, stable_a));
assert!(client_b.native_application_stream_admitted_for_transport(&connection_id, stable_b));
{
let guard = client_a.offline_runtime.lock().await;
let runtime = guard.as_ref().unwrap();
assert!(!runtime.candidates["device-b"].reachable);
runtime
.trust
.lock()
.unwrap()
.trust()
.credential("device-b", now + 1)
.expect("observation expiry must retain signed trust");
}
assert!(client_a
.disconnect_transport_generation_with_reason(
endpoint_b.id(),
stable_a,
"offline-observation-expiry-test-loss",
)
.await
.unwrap());
let closed_deadline = tokio::time::Instant::now() + std::time::Duration::from_secs(3);
loop {
if client_a.get_connection(endpoint_b.id()).await.is_none()
&& client_b.get_connection(endpoint_a.id()).await.is_none()
{
break;
}
assert!(
tokio::time::Instant::now() < closed_deadline,
"expired observation transport did not close"
);
tokio::time::sleep(std::time::Duration::from_millis(25)).await;
}
tokio::time::sleep(std::time::Duration::from_millis(1_200)).await;
assert!(
client_a.get_connection(endpoint_b.id()).await.is_none()
&& client_b.get_connection(endpoint_a.id()).await.is_none(),
"expired observation must not keep retrying an unreachable candidate"
);
let (rediscovered_a, rediscovered_b) = tokio::join!(
client_a.observe_offline_lan_candidate(addr_b.clone()),
client_b.observe_offline_lan_candidate(addr_a.clone()),
);
assert!(rediscovered_a.unwrap());
assert!(rediscovered_b.unwrap());
let rediscovery_deadline = tokio::time::Instant::now() + std::time::Duration::from_secs(12);
loop {
let stable_a = client_a
.connection_manager
.get_by_connection_id(&connection_id)
.await
.and_then(|record| record.transport_stable_id);
let stable_b = client_b
.connection_manager
.get_by_connection_id(&connection_id)
.await
.and_then(|record| record.transport_stable_id);
let ready_a = stable_a.is_some_and(|stable_id| {
client_a.native_application_stream_admitted_for_transport(&connection_id, stable_id)
});
let ready_b = stable_b.is_some_and(|stable_id| {
client_b.native_application_stream_admitted_for_transport(&connection_id, stable_id)
});
if ready_a && ready_b {
break;
}
assert!(
tokio::time::Instant::now() < rediscovery_deadline,
"rediscovered offline peers did not complete a fresh proof"
);
tokio::time::sleep(std::time::Duration::from_millis(25)).await;
}
{
let guard = client_a.offline_runtime.lock().await;
let runtime = guard.as_ref().unwrap();
assert!(runtime.desired_revision > revision_before_reinstall);
assert!(runtime.candidates["device-b"].reachable);
assert!(std::sync::Arc::ptr_eq(&runtime.trust, &trust_a_owner));
}
let revoked = OfflineTrustBundle::issue(
&issuer,
"field-live",
2,
Some(first_bundle_digest),
None,
vec![credential_a],
vec![credential_b.body.serial],
)
.unwrap();
client_a
.offline()
.apply_trust_bundle(revoked, now + 1)
.await
.unwrap();
let retirement_deadline = tokio::time::Instant::now() + std::time::Duration::from_secs(5);
loop {
let retired = client_a.get_connection(endpoint_b.id()).await.is_none()
&& client_a.connection_manager.list_active().await.is_empty()
&& matches!(
client_a.session_admission(&connection_id),
crate::session_token::SessionAdmission::Pending
);
if retired {
break;
}
assert!(
tokio::time::Instant::now() < retirement_deadline,
"signed revocation did not retire the exact offline route"
);
tokio::time::sleep(std::time::Duration::from_millis(25)).await;
}
client_b.stop_external_auto_connect().await;
let stopped_error = client_b
.offline()
.install_runtime(OfflineRuntimeConfig {
local_device_id: "device-b".to_string(),
signer: signer_b,
trust: trust_b_owner,
})
.await
.expect_err("same-config install must not hide a stopped actor")
.to_string();
assert!(stopped_error.contains("owner is no longer current"));
std::fs::remove_dir_all(state_dir).unwrap();
}
#[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",
);
}
#[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.clone(),
Some("web-node".to_string()),
None,
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"
);
assert_eq!(
client
.managed_connection_device_hint(&web_connection_id)
.await,
None,
"an unbound connection must not borrow a sibling identity through the shared local node"
);
}
#[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(BridgeAction::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, ConnectionStatus::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::coordination::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, ManagedHealth::Dead);
assert!(!snapshot.settled_ready);
assert_eq!(
client
.managed_connection_bridge_action("conn-timeout-1")
.await,
Some(BridgeAction::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.openrtc_generation();
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::AdmissionMechanism::SessionToken,
Some(crate::session_token::GrantScope::from("user-device")),
None,
);
client_b.bind_inbound_native_admission_stream_contract(
&connection_id,
crate::native_protocol::TokenStreamContract::PersistentControl,
);
client_b.bind_outbound_native_admission_stream_contract(
&connection_id,
crate::native_protocol::TokenStreamContract::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_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::coordination::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, ManagedHealth::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(BridgeAction::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
);
}
#[tokio::test]
async fn incoming_local_timeout_close_enters_recoverable_replacement_handoff() {
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.state, ConnectionState::Connected);
assert_eq!(record.transport_stable_id, None);
assert_eq!(
record.status_reason.as_deref(),
Some(crate::lifecycle_reason::REASON_REPLACEMENT_IN_PROGRESS)
);
assert_eq!(
record.transport_source.as_deref(),
Some("incoming-transport-lost:Some(TimedOut)")
);
assert_eq!(record.last_disconnect_reason, None);
let peer = client
.peer_session("device-timeout-close")
.await
.expect("recoverable peer session");
assert!(peer.replacement_pending);
assert!(!peer.settled_ready);
}
#[tokio::test]
async fn incoming_explicit_replacement_close_preserves_the_owned_handoff() {
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-explicit-replacement".to_string(),
Some("node-explicit-replacement".to_string()),
Some("device-explicit-replacement".to_string()),
Some("node-explicit-replacement".to_string()),
)
.await;
client
.connection_manager
.set_connected_with_transport(
"conn-explicit-replacement",
Some("node-explicit-replacement".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-explicit-replacement",
endpoint_id,
"node-explicit-replacement",
10,
crate::lifecycle_reason::REASON_REPLACED_BY_NEW_OUTBOUND,
)
.await;
assert_eq!(
resolution,
IncomingTransportCloseResolution::PreservedKeptTransport
);
let record = client
.connection_manager
.get_by_connection_id("conn-explicit-replacement")
.await
.expect("record");
assert_eq!(record.state, ConnectionState::Connected);
assert_eq!(record.transport_stable_id, None);
assert_eq!(
record.status_reason.as_deref(),
Some(crate::lifecycle_reason::REASON_REPLACEMENT_IN_PROGRESS)
);
}
#[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.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_host_admission_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.connection_state_updates();
client
.connection_manager
.upsert_pending(
"conn-admission-event".to_string(),
Some("node-admission-event".to_string()),
None,
Some("node-admission-event".to_string()),
)
.await;
client
.connection_manager
.set_connected_with_transport(
"conn-admission-event",
Some("node-admission-event".to_string()),
Some(43),
Some("incoming".to_string()),
)
.await
.expect("connected record");
client
.mark_connection_admitted_by_host(
"conn-admission-event",
None,
Some("device-admission-event".to_string()),
)
.await;
let update = tokio::time::timeout(std::time::Duration::from_secs(1), updates.recv())
.await
.expect("admission update timeout")
.expect("admission update");
assert_eq!(update.connection_id, "conn-admission-event");
assert_eq!(update.device_id.as_deref(), Some("device-admission-event"));
}
#[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 admitted_application_activity_restores_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),
);
let connection_id = "conn-application-activity";
client.session_token_registry.mark_accepted(
connection_id,
crate::session_token::AdmissionMechanism::SessionToken,
Some(crate::session_token::GrantScope::from("user-device")),
Some("device-application-activity".to_string()),
);
client.bind_inbound_native_admission_stream_contract(
connection_id,
crate::native_protocol::TokenStreamContract::OneShotAdmission,
);
client.bind_inbound_session_token_transport(connection_id, 42);
client
.connection_manager
.upsert_pending(
connection_id.to_string(),
Some("node-application-activity".to_string()),
Some("device-application-activity".to_string()),
Some("node-application-activity".to_string()),
)
.await;
client
.connection_manager
.set_connected_with_transport(
connection_id,
Some("node-application-activity".to_string()),
Some(42),
Some("incoming".to_string()),
)
.await
.expect("connected record");
client
.connection_manager
.set_health_if_current(connection_id, 42, ConnectionHealth::Stale)
.await
.expect("stale current generation");
assert!(
client
.confirm_native_application_stream_activity(connection_id, 42)
.await,
"admitted traffic on the exact current generation is positive liveness proof",
);
let restored = client
.connection_manager
.peer_snapshot(connection_id)
.await
.expect("restored peer");
assert_eq!(restored.health, ConnectionHealth::Healthy);
client
.connection_manager
.mark_transport_replaced(
connection_id,
Some(43),
Some("incoming".to_string()),
Some(crate::lifecycle_reason::REASON_REPLACEMENT_IN_PROGRESS.to_string()),
)
.await
.expect("replacement record");
client
.connection_manager
.set_health_if_current(connection_id, 43, ConnectionHealth::Stale)
.await
.expect("stale replacement generation");
assert!(
!client
.confirm_native_application_stream_activity(connection_id, 42)
.await,
"traffic attributed to a retired generation cannot revive its replacement",
);
let replacement = client
.connection_manager
.peer_snapshot(connection_id)
.await
.expect("replacement peer");
assert_eq!(replacement.health, ConnectionHealth::Stale);
assert_eq!(
client.native_transport_protocol_activity_age(connection_id, 42),
None,
"failed stale-generation confirmation must not leave reusable liveness evidence",
);
}
#[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.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::AdmissionMechanism::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::TokenStreamContract::PersistentControl,
);
client.bind_outbound_native_admission_stream_contract(
"conn-terminal",
crate::native_protocol::TokenStreamContract::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.bind_connection_application_crypto_transport("conn-terminal", 42);
client.confirm_connection_application_crypto("conn-terminal", 42);
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", Some(42)));
assert!(!client
.connection_application_key_agreements
.read()
.expect("application key agreement registry")
.contains_key("conn-terminal"));
}
#[cfg(all(not(target_arch = "wasm32"), feature = "iroh-carrier-core"))]
#[tokio::test]
async fn native_terminal_retirement_clears_reserved_ble_gate_before_attempt_publication() {
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-terminal-ble-reservation";
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;
let record = client
.connection_manager
.set_connected_with_transport(
connection_id,
Some("node-remote".to_string()),
Some(42),
Some("incoming".to_string()),
)
.await
.expect("connected record");
let generation = crate::client::NativePeerDataGeneration {
transport_stable_id: 42,
transport_generation: record.transport_generation,
route_generation: record.route_generation,
};
let peer_policy_epoch = client.bump_iroh_carrier_peer_policy_epoch(connection_id);
client.native_transport_upgrade_gates.lock().await.insert(
(connection_id.to_string(), IrohPathKind::Ble),
crate::client::NativeTransportUpgradeGate {
upgrade_id: "reserved-before-attempt".to_string(),
generation,
policy_epoch: client.current_iroh_carrier_policy_epoch(),
peer_policy_epoch,
},
);
client.native_transport_upgrade_gates.lock().await.insert(
(connection_id.to_string(), IrohPathKind::WebRtc),
crate::client::NativeTransportUpgradeGate {
upgrade_id: "reserved-carrier-before-attempt".to_string(),
generation,
policy_epoch: client.current_iroh_carrier_policy_epoch(),
peer_policy_epoch,
},
);
client
.connection_manager
.set_closed_if_current(
connection_id,
generation.transport_stable_id,
Some(crate::lifecycle_reason::REASON_MANUAL_DISCONNECT.to_string()),
)
.await
.expect("terminal generation");
client
.retire_managed_connection(
connection_id,
Some(crate::lifecycle_reason::REASON_MANUAL_DISCONNECT.to_string()),
)
.await;
assert!(!client
.native_transport_upgrade_gates
.lock()
.await
.keys()
.any(|(candidate_connection_id, _)| candidate_connection_id == connection_id));
assert!(!client
.native_ble_upgrade_attempts
.lock()
.await
.contains_key(connection_id));
assert_eq!(
client.current_iroh_carrier_peer_policy_epoch(connection_id),
0,
"terminal retirement must retire the peer-scoped policy revision with its last gate",
);
}
#[cfg(all(not(target_arch = "wasm32"), feature = "iroh-carrier-core"))]
#[tokio::test]
async fn late_native_ble_retirement_cleanup_preserves_reused_connection_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 = "conn-reused-after-ble-retirement";
let retiring_generation = crate::client::NativePeerDataGeneration {
transport_stable_id: 41,
transport_generation: 1,
route_generation: 2,
};
let successor_generation = crate::client::NativePeerDataGeneration {
transport_stable_id: 42,
transport_generation: 2,
route_generation: 0,
};
let retiring_peer_policy_epoch = client.bump_iroh_carrier_peer_policy_epoch(connection_id);
client.native_transport_upgrade_gates.lock().await.insert(
(connection_id.to_string(), IrohPathKind::Ble),
crate::client::NativeTransportUpgradeGate {
upgrade_id: "retiring-upgrade".to_string(),
generation: retiring_generation,
policy_epoch: client.current_iroh_carrier_policy_epoch(),
peer_policy_epoch: retiring_peer_policy_epoch,
},
);
client.native_ble_upgrade_attempts.lock().await.insert(
connection_id.to_string(),
crate::client::NativeBleUpgradeAttemptState {
generation: retiring_generation,
attempts: 1,
},
);
client
.retire_native_carrier_upgrade_state_if_current(
connection_id,
Some((
Some(retiring_generation.transport_stable_id),
retiring_generation.transport_generation,
retiring_generation.route_generation,
)),
Some(retiring_peer_policy_epoch),
)
.await;
assert!(!client
.native_transport_upgrade_gates
.lock()
.await
.contains_key(&(connection_id.to_string(), IrohPathKind::Ble)));
assert!(!client
.native_ble_upgrade_attempts
.lock()
.await
.contains_key(connection_id));
assert_eq!(
client.current_iroh_carrier_peer_policy_epoch(connection_id),
0,
);
let successor_peer_policy_epoch = client.bump_iroh_carrier_peer_policy_epoch(connection_id);
client.native_transport_upgrade_gates.lock().await.insert(
(connection_id.to_string(), IrohPathKind::Ble),
crate::client::NativeTransportUpgradeGate {
upgrade_id: "successor-upgrade".to_string(),
generation: successor_generation,
policy_epoch: client.current_iroh_carrier_policy_epoch(),
peer_policy_epoch: successor_peer_policy_epoch,
},
);
client.native_ble_upgrade_attempts.lock().await.insert(
connection_id.to_string(),
crate::client::NativeBleUpgradeAttemptState {
generation: successor_generation,
attempts: 1,
},
);
client
.retire_native_carrier_upgrade_state_if_current(
connection_id,
Some((
Some(retiring_generation.transport_stable_id),
retiring_generation.transport_generation,
retiring_generation.route_generation,
)),
Some(retiring_peer_policy_epoch),
)
.await;
assert_eq!(
client
.native_transport_upgrade_gates
.lock()
.await
.get(&(connection_id.to_string(), IrohPathKind::Ble))
.map(|gate| (gate.upgrade_id.clone(), gate.generation)),
Some(("successor-upgrade".to_string(), successor_generation)),
);
assert_eq!(
client
.native_ble_upgrade_attempts
.lock()
.await
.get(connection_id)
.copied(),
Some(crate::client::NativeBleUpgradeAttemptState {
generation: successor_generation,
attempts: 1,
}),
);
assert_eq!(
client.current_iroh_carrier_peer_policy_epoch(connection_id),
successor_peer_policy_epoch,
"late retirement must not erase the successor connection's policy revision",
);
}
#[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::AdmissionMechanism::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::TokenStreamContract::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.bind_connection_application_crypto_transport(connection_id, 42);
client.confirm_connection_application_crypto(connection_id, 42);
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::TokenStreamContract::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, Some(42)));
}
#[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
);
}
#[test]
fn native_non_initiator_wakes_at_the_policy_deadline() {
let waits = HashMap::from([
("device-a".to_string(), 11_000_i64),
("device-b".to_string(), 12_000_i64),
]);
let escalations = HashMap::from([("device-a".to_string(), 1_u8)]);
assert_eq!(
next_non_initiator_wakeup_delay_ms(&waits, &escalations, 12_500),
Some(1_240),
);
assert_eq!(
next_non_initiator_wakeup_delay_ms(&waits, &escalations, 20_000),
Some(1),
);
assert_eq!(
next_non_initiator_wakeup_delay_ms(&HashMap::new(), &HashMap::new(), 0),
None,
);
}
#[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, ManagedHealth::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, ManagedHealth::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, ManagedHealth::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(BridgeAction::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!(before.replacement_pending);
assert_eq!(before.readiness_state, ReadinessState::AwaitingReplacement);
assert_eq!(before.readiness_reason, "missing-active-transport");
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_manual_disconnect_settles_owned_replacement_handoff() {
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-manual-handoff".to_string(),
Some("node-manual-handoff".to_string()),
Some("device-manual-handoff".to_string()),
Some("node-manual-handoff".to_string()),
)
.await;
client
.connection_manager
.set_connected_with_transport(
"conn-manual-handoff",
Some("node-manual-handoff".to_string()),
Some(10),
Some("incoming".to_string()),
)
.await
.expect("connected record");
client
.connection_manager
.mark_transport_replaced_if_current(
"conn-manual-handoff",
10,
Some("incoming-replacement-churn".to_string()),
Some(crate::lifecycle_reason::REASON_REPLACEMENT_IN_PROGRESS.to_string()),
)
.await
.expect("owned handoff");
let endpoint_id = "5d388fcaaa61efa536d78051e060928f6c1fff0983a32f6b532176b12328c3ac"
.parse()
.expect("endpoint id");
let resolution = client
.reconcile_incoming_transport_after_local_close(
"conn-manual-handoff",
endpoint_id,
"node-manual-handoff",
10,
crate::lifecycle_reason::REASON_MANUAL_DISCONNECT,
)
.await;
assert_eq!(
resolution,
IncomingTransportCloseResolution::RetiredClosedTransport
);
let record = client
.connection_manager
.get_by_connection_id("conn-manual-handoff")
.await
.expect("record");
assert_eq!(record.state, ConnectionState::Closed);
assert_eq!(record.transport_stable_id, None);
assert_eq!(
record.status_reason.as_deref(),
Some(crate::lifecycle_reason::REASON_MANUAL_DISCONNECT)
);
assert_eq!(
record.last_disconnect_reason.as_deref(),
Some(crate::lifecycle_reason::REASON_MANUAL_DISCONNECT)
);
}
#[tokio::test]
async fn reconcile_incoming_revocation_allows_fresh_drive_grant_reauthorization() {
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-revoked-drive-grant";
let endpoint_id: iroh::EndpointId =
"5d388fcaaa61efa536d78051e060928f6c1fff0983a32f6b532176b12328c3ac"
.parse()
.expect("endpoint id");
client
.connection_manager
.upsert_pending(
connection_id.to_string(),
Some(endpoint_id.to_string()),
Some("device-revoked-drive-grant".to_string()),
Some(endpoint_id.to_string()),
)
.await;
client
.connection_manager
.set_connected_with_transport(
connection_id,
Some(endpoint_id.to_string()),
Some(10),
Some("incoming".to_string()),
)
.await
.expect("connected record");
client
.connection_manager
.add_scope(connection_id, "drive-grant:revoked")
.await;
let resolution = client
.reconcile_incoming_transport_after_local_close(
connection_id,
endpoint_id,
"node-revoked-drive-grant",
10,
r#"ApplicationClosed(ApplicationClose { error_code: 1, reason: b\"session-token-revoked\" })"#,
)
.await;
assert_eq!(
resolution,
IncomingTransportCloseResolution::RetiredClosedTransport
);
let revoked = client
.connection_manager
.get_by_connection_id(connection_id)
.await
.expect("revoked record");
assert_eq!(revoked.state, ConnectionState::Closed);
assert_eq!(revoked.transport_stable_id, None);
assert_eq!(
revoked.status_reason.as_deref(),
Some(crate::lifecycle_reason::REASON_SESSION_TOKEN_REVOKED)
);
assert_eq!(
revoked.last_disconnect_reason.as_deref(),
Some(crate::lifecycle_reason::REASON_SESSION_TOKEN_REVOKED)
);
client
.connection_manager
.remove(connection_id)
.await
.expect("retired revoked record");
let reopened = client
.connection_manager
.commit_admitted_transport(
connection_id,
Some(endpoint_id.to_string()),
Some("device-revoked-drive-grant".to_string()),
11,
Some("incoming-admission".to_string()),
|requires_authenticated_peer_reconnect, previous| {
assert!(!requires_authenticated_peer_reconnect);
assert_eq!(previous, None);
true
},
|scopes| {
scopes.clear();
scopes.insert("drive-grant:fresh".to_string());
},
)
.await
.expect("fresh drive grant reopens revoked session");
assert_eq!(reopened.state, ConnectionState::Connected);
assert_eq!(reopened.transport_stable_id, Some(11));
assert_eq!(
client.connection_manager.get_scopes(connection_id).await,
vec!["drive-grant:fresh".to_string()]
);
}
#[cfg(not(target_arch = "wasm32"))]
#[tokio::test]
async fn reconcile_incoming_timeout_preserves_raced_path_watcher_handoff() {
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-handoff".to_string(),
Some("node-timeout-handoff".to_string()),
Some("device-timeout-handoff".to_string()),
Some("node-timeout-handoff".to_string()),
)
.await;
client
.connection_manager
.set_connected_with_transport(
"conn-timeout-handoff",
Some("node-timeout-handoff".to_string()),
Some(23),
Some("incoming".to_string()),
)
.await
.expect("connected record");
client
.connection_manager
.mark_transport_replaced_if_current(
"conn-timeout-handoff",
23,
Some("iroh-connection-closed".to_string()),
Some(crate::lifecycle_reason::REASON_REPLACEMENT_IN_PROGRESS.to_string()),
)
.await
.expect("path watcher handoff");
let endpoint_id = "5d388fcaaa61efa536d78051e060928f6c1fff0983a32f6b532176b12328c3ac"
.parse()
.expect("endpoint id");
let resolution = client
.reconcile_incoming_transport_after_local_close(
"conn-timeout-handoff",
endpoint_id,
"node-timeout-handoff",
23,
"TimedOut",
)
.await;
assert_eq!(
resolution,
IncomingTransportCloseResolution::PreservedKeptTransport,
);
let record = client
.connection_manager
.get_by_connection_id("conn-timeout-handoff")
.await
.expect("record");
assert_eq!(record.state, ConnectionState::Connected);
assert_eq!(record.transport_stable_id, None);
assert_eq!(
record.status_reason.as_deref(),
Some(crate::lifecycle_reason::REASON_REPLACEMENT_IN_PROGRESS),
);
assert_ne!(record.last_disconnect_reason.as_deref(), Some("TimedOut"));
}
#[cfg(not(target_arch = "wasm32"))]
#[tokio::test]
async fn native_close_watcher_settles_outbound_manual_disconnect_handoff() {
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-outbound-manual-handoff".to_string(),
Some("node-outbound-manual-handoff".to_string()),
Some("device-outbound-manual-handoff".to_string()),
Some("node-outbound-manual-handoff".to_string()),
)
.await;
client
.connection_manager
.set_connected_with_transport(
"conn-outbound-manual-handoff",
Some("node-outbound-manual-handoff".to_string()),
Some(17),
Some("outgoing".to_string()),
)
.await
.expect("connected record");
client
.observe_native_transport_generation_lost(
"conn-outbound-manual-handoff",
17,
"iroh-connection-closed",
)
.await;
assert!(
client
.observe_native_transport_generation_closed(
"conn-outbound-manual-handoff",
17,
Some(crate::lifecycle_reason::REASON_MANUAL_DISCONNECT),
"native-accept-closed",
)
.await,
"the explicit peer close must settle the exact replacement handoff",
);
let record = client
.connection_manager
.get_by_connection_id("conn-outbound-manual-handoff")
.await
.expect("record");
assert_eq!(record.state, ConnectionState::Closed);
assert_eq!(record.transport_stable_id, None);
assert_eq!(
record.last_disconnect_reason.as_deref(),
Some(crate::lifecycle_reason::REASON_MANUAL_DISCONNECT),
);
assert!(client.is_auto_connect_excluded("device-outbound-manual-handoff"));
}
#[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.openrtc_generation();
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_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, ManagedHealth::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(BridgeAction::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.openrtc_generation()),
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, ConnectionStatus::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 repeated_health_observation_does_not_rewrite_projected_lifecycle_time() {
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-observation-timestamp";
client
.connection_manager
.upsert_pending(
connection_id.to_string(),
Some("node-observation".to_string()),
Some("device-observation".to_string()),
Some("node-observation".to_string()),
)
.await;
client
.connection_manager
.set_connected_with_transport(
connection_id,
Some("node-observation".to_string()),
Some(91),
Some("iroh-relay".to_string()),
)
.await
.expect("connected record");
client
.connection_manager
.report_transport_status(connection_id, "iroh-relay".to_string(), None)
.await
.expect("initial route observation");
let first = client
.connection_state(connection_id)
.await
.expect("first projected state");
tokio::time::sleep(Duration::from_millis(2)).await;
client
.connection_manager
.report_transport_status(connection_id, "iroh-relay".to_string(), None)
.await
.expect("repeated health observation");
let repeated = client
.connection_state(connection_id)
.await
.expect("repeated projected state");
assert_eq!(
repeated.last_lifecycle_transition_at_ms, first.last_lifecycle_transition_at_ms,
"a health observation is liveness, not a lifecycle transition",
);
assert!(repeated.updated_at >= first.updated_at);
}
#[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, ConnectionStatus::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_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_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 wait_for_settled_scope_resolves_transient_peer_without_device_alias() {
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-scope";
let scope = "drive-grant:grant-scope";
client
.connection_manager
.upsert_pending(
connection_id.to_string(),
Some("node-drive-grant-scope".to_string()),
None,
Some("node-drive-grant-scope".to_string()),
)
.await;
client
.connection_manager
.set_connected(connection_id, Some("node-drive-grant-scope".to_string()))
.await;
client
.connection_manager
.set_health(
connection_id,
crate::connection_manager::ConnectionHealth::Healthy,
)
.await;
client
.connection_manager
.add_scope(connection_id, scope)
.await;
let snapshot = client
.wait_for_settled_scope(scope, Some(100))
.await
.expect("settled transient scope");
assert_eq!(
snapshot.active_connection_id.as_deref(),
Some(connection_id)
);
assert_eq!(snapshot.device_id, None);
assert!(snapshot.settled_ready);
}
#[tokio::test]
async fn wait_for_settled_scope_matches_exact_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-drive-grant-prefix";
client
.connection_manager
.upsert_pending(
connection_id.to_string(),
Some("node-drive-grant-prefix".to_string()),
None,
Some("node-drive-grant-prefix".to_string()),
)
.await;
client
.connection_manager
.set_connected(connection_id, Some("node-drive-grant-prefix".to_string()))
.await;
client
.connection_manager
.set_health(
connection_id,
crate::connection_manager::ConnectionHealth::Healthy,
)
.await;
client
.connection_manager
.add_scope(connection_id, "drive-grant:grant-10")
.await;
assert!(client
.wait_for_settled_scope("drive-grant:grant-1", Some(10))
.await
.is_none());
}
#[tokio::test]
async fn wait_for_settled_scope_fails_closed_when_scope_is_ambiguous() {
let client = Client::new_with_app_tag(
crate::test_constants::TEST_PROJECT_ID.to_string(),
"test-app".to_string(),
Box::new(|| None),
);
let scope = "drive-grant:ambiguous";
for suffix in ["a", "b"] {
let connection_id = format!("conn-drive-grant-{suffix}");
let node_id = format!("node-drive-grant-{suffix}");
client
.connection_manager
.upsert_pending(
connection_id.clone(),
Some(node_id.clone()),
None,
Some(node_id.clone()),
)
.await;
client
.connection_manager
.set_connected(&connection_id, Some(node_id))
.await;
client
.connection_manager
.set_health(
&connection_id,
crate::connection_manager::ConnectionHealth::Healthy,
)
.await;
client
.connection_manager
.add_scope(&connection_id, scope)
.await;
}
assert!(client
.wait_for_settled_scope(scope, Some(10))
.await
.is_none());
}
#[tokio::test]
async fn settled_scope_rejects_recent_in_place_generation_without_candidate_ids() {
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-generation";
let node_id = "844465046e89fce03a8f96df9e289ab1a4336f05d1d4ef67f2d4598dd7ad0a2a";
let scope = "drive-grant:generation";
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(41),
Some("iroh-relay".to_string()),
)
.await
.expect("initial generation");
client
.connection_manager
.set_health(connection_id, ConnectionHealth::Healthy)
.await;
client
.connection_manager
.add_scope(connection_id, scope)
.await;
let captured = client
.peer_session(connection_id)
.await
.expect("captured generation");
client
.connection_manager
.set_connected_with_transport(
connection_id,
Some(node_id.to_string()),
Some(42),
Some("webrtc-upgrade".to_string()),
)
.await
.expect("in-place replacement generation");
client
.connection_manager
.set_health(connection_id, ConnectionHealth::Healthy)
.await;
let recent = client
.peer_session(connection_id)
.await
.expect("recent replacement snapshot");
assert!(recent.settled_ready);
assert!(recent.candidate_connection_ids.is_empty());
assert_eq!(recent.transport_generation, 2);
assert!(
!Client::peer_stream_generation_matches(&captured, &recent, 41),
"an in-flight opener must reject the same logical connection after its generation advances"
);
assert!(
client
.wait_for_settled_scope(scope, Some(10))
.await
.is_none(),
"a scope must not expose a recently replaced generation as stable"
);
let error = client
.resolve_settled_peer_endpoint(connection_id, Some(10))
.await
.expect_err("protected open must fail before using the recent generation");
assert!(
error.to_string().contains("peer-stream-route-pending"),
"unexpected error: {error:#}",
);
}
#[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_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_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_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
.open_inbound_frame(
"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");
}
#[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 HandshakeBinding"
);
}
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("refreshed-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.bind_connection_application_crypto_transport(connection_id, 1);
client.confirm_connection_application_crypto(connection_id, 1);
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(
"refreshed-trusted-token",
connection_id,
)
.await
.expect("same-scope refreshed token should commit");
assert_eq!(
client.connection_application_crypto_key(connection_id),
Some(key),
"a refreshed bearer for the same accepted scope must preserve the key",
);
assert_eq!(
client
.connection_application_crypto_outbound_sequences
.read()
.expect("sequence map should be readable")
.get(connection_id)
.copied(),
Some(1),
"same-scope bearer rotation must preserve the crypto 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, Some(1)));
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()]);
}
#[tokio::test]
async fn claimed_device_binding_is_part_of_one_capability_security_epoch() {
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-atomic-capability-epoch";
let remote_device_id = "remote-installation-1";
let replacement_device_id = "remote-installation-2";
let prior_key = [17u8; crate::application_crypto::APPLICATION_KEY_BYTES];
client.session_token_registry.mark_accepted(
connection_id,
crate::session_token::AdmissionMechanism::SessionToken,
Some(crate::session_token::GrantScope::from("user-device")),
Some(remote_device_id.to_string()),
);
client
.commit_session_token_application_security_epoch(connection_id)
.await;
client.set_connection_application_crypto_key(connection_id, prior_key);
client.bind_connection_application_crypto_transport(connection_id, 1);
client.confirm_connection_application_crypto(connection_id, 1);
client.session_token_registry.mark_accepted(
connection_id,
crate::session_token::AdmissionMechanism::SessionToken,
Some(crate::session_token::GrantScope::from("user-device")),
None,
);
assert!(
client
.bind_admitted_device(connection_id, replacement_device_id)
.await
);
client
.commit_session_token_application_security_epoch(connection_id)
.await;
assert_eq!(
client.connection_application_crypto_key(connection_id),
None,
"the replacement device binding retires the prior epoch once",
);
let settled_key = [23u8; crate::application_crypto::APPLICATION_KEY_BYTES];
client.set_connection_application_crypto_key(connection_id, settled_key);
client.bind_connection_application_crypto_transport(connection_id, 1);
client.confirm_connection_application_crypto(connection_id, 1);
client
.commit_session_token_application_security_epoch(connection_id)
.await;
assert_eq!(
client.connection_application_crypto_key(connection_id),
Some(settled_key),
"post-admission side effects must not rotate crypto again for the same scope and device",
);
}
#[test]
fn outbound_token_rotation_preserves_same_scope_and_retires_changed_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-outbound-security-epoch";
let key = [9u8; crate::application_crypto::APPLICATION_KEY_BYTES];
client.set_connection_application_crypto_key(connection_id, key);
client.bind_connection_application_crypto_transport(connection_id, 1);
client.confirm_connection_application_crypto(connection_id, 1);
client
.connection_application_key_agreements
.write()
.expect("agreement map should be writable")
.insert(
connection_id.to_string(),
crate::key_agreement::KeyAgreement::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, "user-device");
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!(
!client.commit_outbound_session_token_application_security_epoch(
connection_id,
"user-device",
)
);
assert_eq!(
client.connection_application_crypto_key(connection_id),
Some(key),
"a newly minted bearer for the same scope must preserve the key",
);
assert!(
client.commit_outbound_session_token_application_security_epoch(
connection_id,
"drive-grant:replacement",
)
);
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, Some(1)));
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));
}
#[cfg(not(target_arch = "wasm32"))]
#[test]
fn outbound_token_rotation_preserves_fresh_inbound_generation_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 = "conn-bilateral-outbound-security-epoch";
client.bind_inbound_native_admission_stream_contract(
connection_id,
crate::native_protocol::TokenStreamContract::OneShotAdmission,
);
client.bind_outbound_native_admission_stream_contract(
connection_id,
crate::native_protocol::TokenStreamContract::PersistentControl,
);
client.bind_inbound_session_token_transport(connection_id, 2);
client
.remote_session_admission_proofs
.write()
.expect("remote proof registry")
.insert(
connection_id.to_string(),
RemoteSessionAdmissionProof {
transport_stable_id: 1,
token_fingerprint: crate::session_token::token_fingerprint("token-a"),
approval_scope: "user-device".to_string(),
},
);
client
.commit_outbound_session_token_application_security_epoch(connection_id, "user-device");
assert!(!client
.begin_outbound_session_token_application_security_epoch(connection_id, "token-b"));
assert!(
client.commit_outbound_session_token_application_security_epoch(
connection_id,
"drive-grant:replacement",
)
);
assert!(
client.inbound_session_token_admitted_for_transport(connection_id, 2),
"the peer's current receiver-owned proof must survive our token rotation",
);
let contracts = client.native_admission_stream_contracts(connection_id);
assert_eq!(
contracts.inbound,
Some(crate::native_protocol::TokenStreamContract::OneShotAdmission),
);
assert_eq!(contracts.outbound, None);
assert!(
client
.remote_session_admission_proofs
.read()
.expect("remote proof registry")
.get(connection_id)
.is_none(),
"the presenter-owned proof must be retired until token-b is approved",
);
}
#[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);
}
#[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_ticket(&first);
let (_, second_suffix) = crate::session_token::split_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_ticket(&first);
let (_, second_suffix) = crate::session_token::split_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_ticket(&first);
let (_, second_suffix) = crate::session_token::split_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::AdmissionMechanism::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"
);
}
#[tokio::test]
async fn scope_revocation_keeps_route_alive_until_terminal_delivery_finishes() {
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 = "scope-revoke-terminal-order";
client
.connection_manager
.upsert_pending(connection_id.to_string(), None, None, None)
.await;
client
.connection_manager
.set_connected(connection_id, None)
.await;
client.session_token_registry.mark_accepted(
connection_id,
crate::session_token::AdmissionMechanism::SessionToken,
Some(crate::session_token::GrantScope::from("share:test")),
None,
);
let affected = client.begin_revoke_tokens_by_scope("share:test");
assert_eq!(affected, vec![connection_id.to_string()]);
assert_eq!(
client
.connection_manager
.get_by_connection_id(connection_id)
.await
.expect("connection remains available for terminal delivery")
.state,
ConnectionState::Connected,
);
client
.finish_revoke_tokens_by_scope("share:test", &affected)
.await;
assert_eq!(
client
.connection_manager
.get_by_connection_id(connection_id)
.await
.expect("connection record")
.state,
ConnectionState::Closed,
);
}
#[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_ticket(iroh_ticket, token, "user-device", 0);
let (recovered_ticket, suffix) = crate::session_token::split_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::TokenStreamContract::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::TokenStreamContract::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::TokenStreamContract::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_admitted_device(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));
}
#[cfg(not(target_arch = "wasm32"))]
#[tokio::test]
async fn offline_boot_binds() {
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 connection_id = "conn-initial-offline-device";
let device_id = "remote-device";
client
.start_external_auto_connect("test-user".to_string(), "local-device".to_string())
.await
.expect("external actor starts");
assert!(client
.submit_external_desired_peers(
1,
r#"[{"deviceId":"remote-device","nodeId":"remote-node","online":false}]"#,
)
.await
.expect("initial offline roster is accepted"));
client
.connection_manager
.upsert_pending(
connection_id.to_string(),
Some("remote-node".to_string()),
None,
None,
)
.await;
client
.mark_connection_admitted_by_host(connection_id, Some("user-device"), None)
.await;
assert!(
client.bind_admitted_device(connection_id, device_id).await,
"a durable offline roster snapshot is not a device withdrawal",
);
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
));
}
#[tokio::test]
async fn transient_scope_refuses_late_authoritative_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 connection_id = "conn-transient-scope-device-claim";
let scope = "drive-grant:grant-1";
let device_id = "untrusted-transient-device-claim";
client
.connection_manager
.upsert_pending(
connection_id.to_string(),
Some("node-1".to_string()),
None,
Some("node-1".to_string()),
)
.await;
client
.mark_connection_admitted_by_host(connection_id, Some(scope), None)
.await;
assert!(
!client.bind_admitted_device(connection_id, device_id).await,
"transient scoped tickets must not promote a claimed device into durable identity",
);
assert!(matches!(
client.session_admission(connection_id),
crate::session_token::SessionAdmission::Accepted {
scope: Some(admitted_scope),
authoritative_device_id: None,
..
} if admitted_scope.as_str() == scope
));
let record = client
.connection_manager
.get_by_connection_id(connection_id)
.await
.expect("managed connection record");
assert_eq!(record.device_id, None);
}
#[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::AdmissionMechanism::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_ticket(iroh_ticket, token, "user-device", 0);
let (recovered_iroh, suffix) = crate::session_token::split_ticket(&compound);
assert_eq!(recovered_iroh, iroh_ticket);
let extracted_token = suffix
.and_then(|value| crate::session_token::decode_payload(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(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));
}
#[test]
fn physical_transport_replacement_requires_fresh_application_crypto_confirmation() {
assert!(!transport_replacement_requires_application_crypto_reconfirmation(None, 2,));
assert!(!transport_replacement_requires_application_crypto_reconfirmation(Some(2), 2,));
assert!(transport_replacement_requires_application_crypto_reconfirmation(Some(1), 2,));
}
#[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::AdmissionMechanism::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::TokenStreamContract::PersistentControl,
);
client.bind_outbound_native_admission_stream_contract(
connection_id,
crate::native_protocol::TokenStreamContract::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(),
},
);
let application_key = [43u8; crate::application_crypto::APPLICATION_KEY_BYTES];
client.set_connection_application_crypto_required(connection_id);
client.set_connection_application_crypto_key(connection_id, application_key);
let _ = client.get_or_create_connection_key_agreement(connection_id);
client.bind_connection_application_crypto_transport(connection_id, 10);
client.confirm_connection_application_crypto(connection_id, 10);
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_eq!(
client.connection_application_crypto_key(connection_id),
Some(application_key),
"transient replacement keeps the negotiated application key",
);
assert!(
!client.connection_application_crypto_is_confirmed(connection_id, Some(10)),
"retiring the old physical route must retire its key confirmation",
);
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::AdmissionMechanism::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::TokenStreamContract::PersistentControl,
);
client.bind_outbound_native_admission_stream_contract(
connection_id,
crate::native_protocol::TokenStreamContract::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::AdmissionMechanism::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::TokenStreamContract::PersistentControl,
);
client.bind_outbound_native_admission_stream_contract(
connection_id,
crate::native_protocol::TokenStreamContract::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.bind_connection_application_crypto_transport(connection_id, 10);
client.confirm_connection_application_crypto(connection_id, 10);
assert!(client.connection_application_crypto_is_confirmed(connection_id, Some(10)));
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, Some(11)),
"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 logical_transport_rebind_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::AdmissionMechanism::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::TokenStreamContract::PersistentControl,
);
client.bind_outbound_native_admission_stream_contract(
connection_id,
crate::native_protocol::TokenStreamContract::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.set_connection_application_crypto_required(connection_id);
client.confirm_connection_application_crypto(connection_id, 10);
assert!(
!client.connection_application_crypto_is_confirmed(connection_id, Some(10)),
"a caller-supplied first ACK cannot create a binding before the Rust owner proves the stream generation",
);
client.bind_connection_application_crypto_transport(connection_id, 10);
client.confirm_connection_application_crypto(connection_id, 10);
assert!(client.connection_application_crypto_is_confirmed(connection_id, Some(10)));
client
.connection_manager
.commit_current_transport(
connection_id,
Some(remote_node_id.to_string()),
10,
Some("same-physical-generation".to_string()),
|_| client.bind_connection_application_crypto_transport(connection_id, 10),
)
.await;
assert!(
client.connection_application_crypto_is_confirmed(connection_id, Some(10)),
"an idempotent commit of the same physical generation must preserve confirmation",
);
let (old_ack_latched_tx, old_ack_latched_rx) = oneshot::channel();
let (release_old_ack_tx, release_old_ack_rx) = oneshot::channel();
let old_ack_client = client.clone();
let old_ack = tokio::spawn(async move {
old_ack_latched_tx
.send(())
.expect("old ACK reaches the replacement boundary");
release_old_ack_rx
.await
.expect("replacement commit releases old ACK");
old_ack_client.confirm_connection_application_crypto(connection_id, 10);
});
old_ack_latched_rx
.await
.expect("old ACK is latched before replacement");
client
.retire_stale_native_main_route(connection_id, 11)
.await;
client
.connection_manager
.commit_current_transport(
connection_id,
Some(remote_node_id.to_string()),
11,
Some("physical-arbitration-reconcile".to_string()),
|_| client.bind_connection_application_crypto_transport(connection_id, 11),
)
.await;
release_old_ack_tx
.send(())
.expect("release old ACK after replacement publication");
old_ack.await.expect("old ACK task");
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.connection_application_crypto_is_confirmed(connection_id, Some(11)),
"an old ACK latched across replacement must not confirm the new physical generation",
);
assert!(
!client.connection_application_crypto_is_confirmed(connection_id, Some(10)),
"the old ACK must not resurrect the displaced physical generation",
);
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",
);
client.confirm_connection_application_crypto(connection_id, 11);
assert!(client.connection_application_crypto_is_confirmed(connection_id, Some(11)));
client.clear_connection_application_crypto_confirmation(connection_id);
assert!(
!client.connection_application_crypto_is_confirmed(connection_id, Some(11)),
"an explicit lifecycle retirement must clear the prior generation's confirmation before manager readiness",
);
client.bind_inbound_session_token_transport(connection_id, 11);
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!(
!client.native_application_stream_admitted_for_transport(connection_id, 11),
"even after both new route proofs settle, the replacement must remain non-ready until its crypto transcript is confirmed",
);
}
#[cfg(feature = "iroh-carrier-core")]
#[tokio::test]
async fn carrier_replacement_binds_crypto_before_a_newer_winner_can_commit() {
let client = Client::new_with_app_tag(
crate::test_constants::TEST_PROJECT_ID.to_string(),
"carrier-crypto-linearization".to_string(),
Box::new(|| None),
);
let connection_id = "carrier-crypto-linearization-connection";
let endpoint_id = "carrier-crypto-linearization-endpoint";
client
.connection_manager
.upsert_pending(
connection_id.to_string(),
Some(endpoint_id.to_string()),
None,
Some(endpoint_id.to_string()),
)
.await;
let generation_a = client
.connection_manager
.set_connected_with_transport(
connection_id,
Some(endpoint_id.to_string()),
Some(10),
Some("iroh-relay".to_string()),
)
.await
.expect("generation A");
client.set_connection_application_crypto_required(connection_id);
client.set_connection_application_crypto_key(connection_id, [3_u8; 32]);
client.bind_connection_application_crypto_transport(connection_id, 10);
client.confirm_connection_application_crypto(connection_id, 10);
let generation_b = client
.begin_crypto_bound_transport_replacement_commit(
connection_id,
10,
generation_a.transport_generation,
generation_a.route_generation,
11,
Some("webrtc-upgrade".to_string()),
)
.await
.expect("generation B commit begins");
assert!(
!client.connection_application_crypto_is_confirmed(connection_id, Some(11)),
"generation B crypto must already be rebound before its manager guard can publish",
);
let newer_client = client.clone();
let (newer_started_tx, newer_started_rx) = oneshot::channel();
let generation_c = tokio::spawn(async move {
newer_started_tx.send(()).expect("signal C attempt");
newer_client
.begin_crypto_bound_transport_replacement_commit(
connection_id,
11,
generation_a.transport_generation.saturating_add(1),
generation_a.route_generation,
12,
Some("moq-upgrade".to_string()),
)
.await
.expect("generation C commit begins")
});
newer_started_rx.await.expect("generation C started");
tokio::task::yield_now().await;
assert!(
!generation_c.is_finished(),
"generation C must wait for B's manager+crypto transaction",
);
assert_eq!(generation_b.finish().transport_stable_id, Some(11));
let generation_c = generation_c.await.expect("generation C task");
assert!(
!client.connection_application_crypto_is_confirmed(connection_id, Some(12)),
"generation C must own the unconfirmed crypto binding before publication",
);
assert_eq!(generation_c.finish().transport_stable_id, Some(12));
assert_eq!(
client
.connection_manager
.get_by_connection_id(connection_id)
.await
.and_then(|record| record.transport_stable_id),
Some(12),
);
client.confirm_connection_application_crypto(connection_id, 11);
assert!(
!client.connection_application_crypto_is_confirmed(connection_id, Some(11)),
"a suspended generation B confirmation cannot replace generation C's binding",
);
}
#[cfg(not(target_arch = "wasm32"))]
#[tokio::test]
async fn late_inbound_admission_commit_cannot_resurrect_a_retired_transport() {
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 = "late-inbound-admission";
let remote_node_id = "5d388fcaaa61efa536d78051e060928f6c1fff0983a32f6b532176b12328c3ac";
let remote_endpoint_id = remote_node_id.parse().expect("endpoint id");
let stale_transport_stable_id = 41;
client
.connection_manager
.upsert_pending(
connection_id.to_string(),
Some(remote_node_id.to_string()),
Some("retired-device".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(stale_transport_stable_id),
Some("incoming".to_string()),
)
.await
.expect("connected record");
client
.connection_manager
.set_closed_if_current(
connection_id,
stale_transport_stable_id,
Some(
crate::lifecycle_reason::REASON_INCOMING_TRANSPORT_CLOSED_WITHOUT_LIVE_REPLACEMENT
.to_string(),
),
)
.await
.expect("retired record");
assert!(
!client
.commit_current_incoming_admission_transport(
connection_id,
remote_endpoint_id,
remote_node_id,
stale_transport_stable_id,
None,
)
.await,
"a verifier result for a missing physical generation must be fenced",
);
let record = client
.connection_manager
.get_by_connection_id(connection_id)
.await
.expect("terminal record");
assert_eq!(record.state, ConnectionState::Closed);
assert_eq!(record.transport_stable_id, None);
assert_eq!(
record.status_reason.as_deref(),
Some(
crate::lifecycle_reason::REASON_INCOMING_TRANSPORT_CLOSED_WITHOUT_LIVE_REPLACEMENT
),
);
assert!(
client.connection_states().await.is_empty(),
"terminal records must not return to the active connection projection",
);
let peer = client
.peer_session("retired-device")
.await
.expect("terminal peer projection");
assert!(!peer.settled_ready);
assert_eq!(peer.status, ConnectionState::Closed);
}
#[cfg(not(target_arch = "wasm32"))]
#[tokio::test]
async fn late_verdict_keeps_post_admission_side_effects_pending_for_the_next_live_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 = "late-verdict-side-effects";
client.register_session_token(
"late-verdict-token".to_string(),
"user-device".to_string(),
0,
);
client
.validate_connection_token("late-verdict-token", connection_id, None)
.await
.expect("verdict-only validation");
assert!(
client.session_token_post_admission_side_effects_pending(connection_id),
"a verdict whose physical generation disappeared before delivery is not a committed application-security epoch",
);
client
.admit_connection_with_token("late-verdict-token", connection_id, None)
.await
.expect("idempotent live-route presentation");
assert!(
!client.session_token_post_admission_side_effects_pending(connection_id),
"the next delivered presentation must commit the previously deferred side effects",
);
}
#[cfg(not(target_arch = "wasm32"))]
#[tokio::test]
async fn scoped_peer_admission_requires_live_iroh_identity_not_manager_hints() {
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 = "hint-is-not-peer-proof";
let endpoint = iroh::SecretKey::generate().public().to_string();
let scope = "room:authority";
client
.session_token_registry
.require_scope_peer_admission(scope)
.unwrap();
client
.session_token_registry
.update_scope_peer_admission(
scope,
1,
crate::session_token::now_unix_ms() + 60_000,
vec![endpoint.clone()],
)
.unwrap();
client.register_session_token("leaked".into(), scope.into(), 1);
client
.connection_manager
.upsert_pending(
connection_id.into(),
Some(endpoint.clone()),
Some("server".into()),
Some(endpoint),
)
.await;
let error = client
.validate_connection_token("leaked", connection_id, None)
.await
.unwrap_err();
assert!(error.contains("unassigned peer"));
client
.mark_connection_admitted_by_host(connection_id, Some(scope), None)
.await;
assert!(matches!(
client.session_admission(connection_id),
crate::session_token::SessionAdmission::Rejected { .. }
));
client.register_session_token("ordinary".into(), "share".into(), 0);
client
.validate_connection_token("ordinary", "ordinary-connection", None)
.await
.unwrap();
}
#[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::AdmissionMechanism::SessionToken,
Some(crate::session_token::GrantScope::from("share:revocable")),
None,
);
client.bind_outbound_native_admission_stream_contract(
connection_id,
crate::native_protocol::TokenStreamContract::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::TokenStreamContract::PersistentControl,
);
client.bind_outbound_native_admission_stream_contract(
connection_id,
crate::native_protocol::TokenStreamContract::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::AdmissionMechanism::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::TokenStreamContract::PersistentControl,
);
client.bind_outbound_native_admission_stream_contract(
connection_id,
crate::native_protocol::TokenStreamContract::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(all(
not(target_arch = "wasm32"),
any(feature = "transport-webrtc", feature = "transport-moq")
))]
#[tokio::test]
async fn carrier_commit_fence_survives_route_projection_but_not_authority_change() {
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 = "carrier-commit-authorization-fence";
let transport_stable_id = 10;
client.register_session_token(
"carrier-fence-token".to_string(),
"user-device".to_string(),
1,
);
client
.validate_session_token_for_connection("carrier-fence-token", connection_id)
.await
.expect("carrier base admission");
client
.commit_session_token_application_security_epoch(connection_id)
.await;
establish_current_managed_user_device_route(&client, connection_id, transport_stable_id);
let _ = client
.get_or_create_connection_key_agreement(connection_id)
.expect("carrier base key agreement");
client.set_connection_application_crypto_key(connection_id, [37u8; 32]);
client.bind_connection_application_crypto_transport(connection_id, transport_stable_id);
client.confirm_connection_application_crypto(connection_id, transport_stable_id);
let expected_generation = crate::client::NativePeerDataGeneration {
transport_stable_id,
transport_generation: 4,
route_generation: 7,
};
let policy_epoch = client.current_iroh_carrier_policy_epoch();
let fence = client
.capture_native_iroh_carrier_authorization_fence(
connection_id,
expected_generation,
IrohPathKind::WebRtc,
policy_epoch,
)
.expect("fully admitted incumbent authorization");
client.native_transport_upgrade_gates.lock().await.insert(
(connection_id.to_string(), IrohPathKind::WebRtc),
crate::client::NativeTransportUpgradeGate {
upgrade_id: "carrier-fence-upgrade".to_string(),
generation: expected_generation,
policy_epoch,
peer_policy_epoch: client.current_iroh_carrier_peer_policy_epoch(connection_id),
},
);
assert!(
client.invalidate_native_main_route_proofs_for_transport(
connection_id,
transport_stable_id,
)
);
assert!(
!client.native_application_stream_admitted_for_transport(
connection_id,
transport_stable_id,
),
"the simulated incumbent close must retire mutable route proofs",
);
assert!(
client.native_iroh_carrier_authorization_epoch_is_current(connection_id, &fence),
"a responder-first incumbent close must not revoke the already proven candidate",
);
assert!(
client
.native_iroh_carrier_pre_ack_authority_is_current(
connection_id,
IrohPathKind::WebRtc,
"carrier-fence-upgrade",
&fence,
)
.await,
"route or successor projection must not retire a still-authorized protected candidate",
);
client.native_transport_upgrade_gates.lock().await.insert(
(connection_id.to_string(), IrohPathKind::WebRtc),
crate::client::NativeTransportUpgradeGate {
upgrade_id: "competing-upgrade".to_string(),
generation: expected_generation,
policy_epoch,
peer_policy_epoch: client.current_iroh_carrier_peer_policy_epoch(connection_id),
},
);
assert!(
!client
.native_iroh_carrier_pre_ack_authority_is_current(
connection_id,
IrohPathKind::WebRtc,
"carrier-fence-upgrade",
&fence,
)
.await,
"a competing upgrade gate must fence the candidate before acknowledgement",
);
client.native_transport_upgrade_gates.lock().await.insert(
(connection_id.to_string(), IrohPathKind::WebRtc),
crate::client::NativeTransportUpgradeGate {
upgrade_id: "carrier-fence-upgrade".to_string(),
generation: expected_generation,
policy_epoch,
peer_policy_epoch: client.current_iroh_carrier_peer_policy_epoch(connection_id),
},
);
client.session_token_registry.mark_accepted(
connection_id,
crate::session_token::AdmissionMechanism::SessionToken,
Some(crate::session_token::GrantScope::from(
"drive-grant:new-epoch",
)),
None,
);
client
.session_token_registry
.commit_application_security_epoch(connection_id);
assert!(
!client.native_iroh_carrier_authorization_epoch_is_current(connection_id, &fence),
"a changed logical authorization epoch must fence the candidate",
);
assert!(
!client
.native_iroh_carrier_pre_ack_authority_is_current(
connection_id,
IrohPathKind::WebRtc,
"carrier-fence-upgrade",
&fence,
)
.await,
"a changed logical authorization epoch must fence acknowledgement",
);
}
#[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::AdmissionMechanism::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::TokenStreamContract::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 pending_reciprocal_user_device_one_shot_requires_current_bilateral_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-one-shot-reciprocal-pending";
client.register_session_token("route-token".to_string(), "user-device".to_string(), 1);
client.session_token_registry.mark_accepted(
connection_id,
crate::session_token::AdmissionMechanism::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::TokenStreamContract::OneShotAdmission,
);
client.bind_inbound_session_token_transport(connection_id, 10);
client
.pending_reciprocal_session_admission_requests
.write()
.expect("reciprocal request registry")
.insert(connection_id.to_string());
assert_eq!(
client.session_admission_block_reason_for_transport(connection_id, Some(10)),
Some((false, "native-main-route-pending".to_string())),
"a typed reciprocal request must close the route until its outbound proof is current",
);
assert!(
client
.native_application_stream_pending_diagnostic(connection_id, 10)
.contains("outbound_required=true"),
"readiness and diagnostics must use the same bilateral predicate",
);
client.bind_outbound_native_admission_stream_contract(
connection_id,
crate::native_protocol::TokenStreamContract::OneShotAdmission,
);
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(),
},
);
client.clear_reciprocal_session_admission_request(connection_id);
assert_eq!(
client.session_admission_block_reason_for_transport(connection_id, Some(10)),
None,
);
}
#[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::AdmissionMechanism::SessionToken,
Some(crate::session_token::GrantScope::from("share")),
None,
);
client.bind_outbound_native_admission_stream_contract(
connection_id,
crate::native_protocol::TokenStreamContract::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::AdmissionMechanism::SessionToken,
Some(crate::session_token::GrantScope::from("share")),
None,
);
client.bind_inbound_native_admission_stream_contract(
connection_id,
crate::native_protocol::TokenStreamContract::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::AdmissionMechanism::SessionToken,
Some(crate::session_token::GrantScope::from("share")),
None,
);
client.bind_inbound_native_admission_stream_contract(
connection_id,
crate::native_protocol::TokenStreamContract::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::AdmissionMechanism::SessionToken,
Some(crate::session_token::GrantScope::from("share")),
None,
);
client.bind_inbound_native_admission_stream_contract(
connection_id,
crate::native_protocol::TokenStreamContract::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.bind_connection_application_crypto_transport(connection_id, 10);
client.confirm_connection_application_crypto(connection_id, 10);
assert!(client.native_application_stream_admitted_for_transport(connection_id, 10));
}
#[cfg(not(target_arch = "wasm32"))]
#[tokio::test]
async fn replacement_capability_epoch_does_not_inherit_bilateral_route_contract() {
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-capability-epoch-switch";
let stable_id = 10;
client.register_session_token(
"old-user-device-token".to_string(),
"user-device".to_string(),
1,
);
client
.validate_session_token_for_connection("old-user-device-token", connection_id)
.await
.expect("old user-device admission");
client
.commit_session_token_application_security_epoch(connection_id)
.await;
client.bind_inbound_native_admission_stream_contract(
connection_id,
crate::native_protocol::TokenStreamContract::PersistentControl,
);
client.bind_outbound_native_admission_stream_contract(
connection_id,
crate::native_protocol::TokenStreamContract::PersistentControl,
);
client.bind_inbound_session_token_transport(connection_id, stable_id);
client
.remote_session_admission_proofs
.write()
.expect("remote proof registry")
.insert(
connection_id.to_string(),
RemoteSessionAdmissionProof {
transport_stable_id: stable_id,
token_fingerprint: crate::session_token::token_fingerprint(
"old-user-device-token",
),
approval_scope: "user-device".to_string(),
},
);
let _ = client
.get_or_create_connection_key_agreement(connection_id)
.expect("old key agreement");
client.set_connection_application_crypto_key(connection_id, [17u8; 32]);
client.bind_connection_application_crypto_transport(connection_id, stable_id);
client.confirm_connection_application_crypto(connection_id, stable_id);
assert!(client.native_application_stream_admitted_for_transport(connection_id, stable_id));
client.register_session_token(
"new-drive-grant-token".to_string(),
"drive-grant:grant-epoch-switch".to_string(),
1,
);
client
.validate_session_token_for_connection("new-drive-grant-token", connection_id)
.await
.expect("replacement drive-grant admission");
client
.commit_session_token_application_security_epoch(connection_id)
.await;
assert_eq!(
client.native_admission_stream_contracts(connection_id),
crate::client::NativeAdmissionStreamContracts::default(),
"the replacement capability must not inherit either directional wire contract",
);
assert!(
!client.inbound_session_token_admitted_for_transport(connection_id, stable_id),
"the replacement capability must not inherit the old inbound proof",
);
assert!(
!client.remote_session_token_admitted_for_transport(connection_id, stable_id),
"the replacement capability must not inherit the old outbound proof",
);
client.bind_inbound_native_admission_stream_contract(
connection_id,
crate::native_protocol::TokenStreamContract::PersistentControl,
);
client.bind_inbound_session_token_transport(connection_id, stable_id);
client.set_connection_application_crypto_required(connection_id);
let _ = client
.get_or_create_connection_key_agreement(connection_id)
.expect("replacement key agreement");
client.set_connection_application_crypto_key(connection_id, [19u8; 32]);
client.bind_connection_application_crypto_transport(connection_id, stable_id);
client.confirm_connection_application_crypto(connection_id, stable_id);
assert!(
client.native_application_stream_admitted_for_transport(connection_id, stable_id),
"a one-way drive grant must become routable from its own current inbound proof",
);
}
#[cfg(not(target_arch = "wasm32"))]
#[tokio::test]
async fn application_route_wait_accepts_a_late_generation_proof_and_fences_key_drift() {
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-wait";
let transport_stable_id = 41;
let key = [23u8; 32];
client.session_token_registry.mark_accepted(
connection_id,
crate::session_token::AdmissionMechanism::SessionToken,
Some(crate::session_token::GrantScope::from(
"drive-grant:route-wait",
)),
None,
);
client.bind_inbound_native_admission_stream_contract(
connection_id,
crate::native_protocol::TokenStreamContract::OneShotAdmission,
);
client.set_connection_application_crypto_required(connection_id);
let _ = client
.get_or_create_connection_key_agreement(connection_id)
.expect("key agreement");
client.set_connection_application_crypto_key(connection_id, key);
client.bind_connection_application_crypto_transport(connection_id, transport_stable_id);
client.confirm_connection_application_crypto(connection_id, transport_stable_id);
assert!(
!client.native_application_stream_admitted_for_transport(
connection_id,
transport_stable_id,
),
"crypto readiness must not hide the missing generation-bound admission proof",
);
let waiting_client = client.clone();
let route_wait = tokio::spawn(async move {
let admission_fingerprint = waiting_client
.session_token_registry
.admission_fingerprint(connection_id);
let security_epoch_fingerprint = waiting_client
.session_token_registry
.application_security_epoch_fingerprint(connection_id);
waiting_client
.wait_for_connection_application_route(
connection_id,
transport_stable_id,
key,
admission_fingerprint.as_deref(),
security_epoch_fingerprint.as_deref(),
std::time::Duration::from_secs(1),
)
.await
});
tokio::task::yield_now().await;
client.bind_inbound_session_token_transport(connection_id, transport_stable_id);
assert!(
route_wait.await.expect("application route wait task"),
"the typed route event must release a product stream once the exact generation is fully admitted",
);
let stale_connection_id = "logical-session-stale-route-wait";
let stale_transport_stable_id = 43;
let stale_key = [29u8; 32];
client.session_token_registry.mark_accepted(
stale_connection_id,
crate::session_token::AdmissionMechanism::SessionToken,
Some(crate::session_token::GrantScope::from(
"drive-grant:stale-route-wait",
)),
None,
);
client.bind_inbound_native_admission_stream_contract(
stale_connection_id,
crate::native_protocol::TokenStreamContract::OneShotAdmission,
);
client.set_connection_application_crypto_required(stale_connection_id);
let _ = client
.get_or_create_connection_key_agreement(stale_connection_id)
.expect("stale key agreement");
client.set_connection_application_crypto_key(stale_connection_id, stale_key);
client.bind_connection_application_crypto_transport(
stale_connection_id,
stale_transport_stable_id,
);
client
.confirm_connection_application_crypto(stale_connection_id, stale_transport_stable_id);
let stale_waiting_client = client.clone();
let stale_wait = tokio::spawn(async move {
let admission_fingerprint = stale_waiting_client
.session_token_registry
.admission_fingerprint(stale_connection_id);
let security_epoch_fingerprint = stale_waiting_client
.session_token_registry
.application_security_epoch_fingerprint(stale_connection_id);
stale_waiting_client
.wait_for_connection_application_route(
stale_connection_id,
stale_transport_stable_id,
stale_key,
admission_fingerprint.as_deref(),
security_epoch_fingerprint.as_deref(),
std::time::Duration::from_secs(1),
)
.await
});
tokio::task::yield_now().await;
client.set_connection_application_crypto_key(stale_connection_id, [31u8; 32]);
assert!(
!stale_wait.await.expect("stale application route wait task"),
"a replacement key must fence and wake the older pending product stream",
);
}
#[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::TokenStreamContract::OneShotAdmission,
);
client.bind_outbound_native_admission_stream_contract(
connection_id,
crate::native_protocol::TokenStreamContract::PersistentControl,
);
assert_eq!(
client.native_admission_stream_contracts(connection_id),
crate::client::NativeAdmissionStreamContracts {
inbound: Some(crate::native_protocol::TokenStreamContract::OneShotAdmission,),
outbound: Some(crate::native_protocol::TokenStreamContract::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::AdmissionMechanism::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::TokenStreamContract::OneShotAdmission,
);
client.bind_outbound_native_admission_stream_contract(
connection_id,
crate::native_protocol::TokenStreamContract::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);
}
#[test]
fn trusted_user_device_application_crypto_policy_is_explicit_and_shared_by_clones() {
let client = Client::new_with_app_tag(
crate::test_constants::TEST_PROJECT_ID.to_string(),
"protected-user-device-policy".to_string(),
Box::new(|| None),
);
assert!(!client.trusted_user_device_application_crypto_is_required());
let clone = client.clone();
client.require_app_crypto(true);
assert!(client.trusted_user_device_application_crypto_is_required());
assert!(clone.trusted_user_device_application_crypto_is_required());
}
#[tokio::test]
async fn carrier_only_configuration_does_not_enable_external_transports() {
let client = Client::new_with_app_tag(
crate::test_constants::TEST_PROJECT_ID.to_string(),
"carrier-only-transport-policy".to_string(),
Box::new(|| None),
);
client
.update_transport_config(crate::client::TransportConfig {
webrtc: Some(crate::client::WebRTCConfig {
..crate::client::WebRTCConfig::default()
}),
moq: Some(crate::client::MoQConfig {
relay_url: "https://127.0.0.1:4443".to_string(),
access_token: None,
}),
..crate::client::TransportConfig::default()
})
.await
.unwrap();
assert!(client.is_webrtc_transport_enabled().await);
assert!(client.is_moq_transport_enabled().await);
assert!(!client.is_webrtc_external_transport_enabled().await);
assert!(!client.is_moq_external_transport_enabled().await);
}
}