2026-02-15 09:47:05 +00:00
|
|
|
//! Integration tests for the iroh-based P2P driver.
|
|
|
|
|
//!
|
|
|
|
|
//! These tests verify that IrohDriver can:
|
|
|
|
|
//! - Create endpoints with matching identities
|
|
|
|
|
//! - Form clusters via join
|
|
|
|
|
//! - Detect membership changes through SWIM
|
2026-02-19 14:39:33 +00:00
|
|
|
//! - Reject unauthorized peers
|
2026-02-15 09:47:05 +00:00
|
|
|
//!
|
2026-06-23 20:10:41 +00:00
|
|
|
//! Runs in the `iroh-driver` crate, where iroh support is always available.
|
2026-02-15 09:47:05 +00:00
|
|
|
|
2026-02-19 14:39:33 +00:00
|
|
|
mod common;
|
2026-02-15 09:47:05 +00:00
|
|
|
|
2026-02-19 14:39:33 +00:00
|
|
|
use std::sync::{Arc, Mutex};
|
|
|
|
|
use std::time::Duration;
|
|
|
|
|
|
|
|
|
|
use common::iroh::*;
|
|
|
|
|
use distribution::peer_auth::PeerAllowList;
|
|
|
|
|
use iroh::PublicKey;
|
|
|
|
|
|
|
|
|
|
// ─── Identity tests ─────────────────────────────────────────────────────
|
2026-02-15 09:47:05 +00:00
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn iroh_driver_creates_with_unique_identity() {
|
2026-02-19 14:39:33 +00:00
|
|
|
let mut d1 = make_driver();
|
|
|
|
|
let mut d2 = make_driver();
|
2026-02-15 09:47:05 +00:00
|
|
|
assert_ne!(d1.node_id(), d2.node_id());
|
|
|
|
|
d1.shutdown();
|
|
|
|
|
d2.shutdown();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
2026-06-09 09:29:07 +00:00
|
|
|
fn iroh_driver_reports_listen_addr_and_no_routes() {
|
|
|
|
|
// A freshly created driver knows where it listens and, having learned no
|
|
|
|
|
// peers, has converged on an empty directory route view.
|
2026-02-19 14:39:33 +00:00
|
|
|
let mut driver = make_driver();
|
2026-06-09 09:29:07 +00:00
|
|
|
assert!(!driver.listen_addr().is_empty());
|
|
|
|
|
assert_eq!(driver.directory_route_count(), 0);
|
2026-02-15 09:47:05 +00:00
|
|
|
driver.shutdown();
|
|
|
|
|
}
|
2026-02-19 14:39:33 +00:00
|
|
|
|
|
|
|
|
// ─── Join integration tests ─────────────────────────────────────────────
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn two_nodes_form_cluster_via_join() {
|
|
|
|
|
let mut node_a = make_driver();
|
|
|
|
|
let mut node_b = make_driver();
|
|
|
|
|
|
|
|
|
|
let b_addr = node_b.endpoint_addr();
|
|
|
|
|
node_a.join(&[b_addr]);
|
|
|
|
|
|
2026-06-23 15:42:28 +00:00
|
|
|
let converged = pump_until_pair(&mut node_a, &mut node_b, Duration::from_secs(5), |a, b| {
|
|
|
|
|
let a_key = PublicKey::from_bytes(&a.node_id().0).unwrap();
|
|
|
|
|
let b_key = PublicKey::from_bytes(&b.node_id().0).unwrap();
|
|
|
|
|
sees_alive(a, &b_key) && sees_alive(b, &a_key)
|
|
|
|
|
});
|
2026-02-19 14:39:33 +00:00
|
|
|
|
|
|
|
|
assert!(converged, "nodes did not converge within timeout");
|
2026-06-09 09:29:07 +00:00
|
|
|
assert_eq!(node_a.alive_count(), 1, "node_a should see 1 alive peer");
|
|
|
|
|
assert_eq!(node_b.alive_count(), 1, "node_b should see 1 alive peer");
|
2026-02-19 14:39:33 +00:00
|
|
|
|
|
|
|
|
node_a.shutdown();
|
|
|
|
|
node_b.shutdown();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn two_nodes_form_cluster_via_mutual_join() {
|
|
|
|
|
let mut node_a = make_driver();
|
|
|
|
|
let mut node_b = make_driver();
|
|
|
|
|
|
|
|
|
|
let b_addr = node_b.endpoint_addr();
|
|
|
|
|
let a_addr = node_a.endpoint_addr();
|
|
|
|
|
|
|
|
|
|
node_a.join(&[b_addr]);
|
|
|
|
|
node_b.join(&[a_addr]);
|
|
|
|
|
|
2026-06-23 15:42:28 +00:00
|
|
|
let converged = pump_until_pair(&mut node_a, &mut node_b, Duration::from_secs(5), |a, b| {
|
|
|
|
|
let a_key = PublicKey::from_bytes(&a.node_id().0).unwrap();
|
|
|
|
|
let b_key = PublicKey::from_bytes(&b.node_id().0).unwrap();
|
|
|
|
|
sees_alive(a, &b_key) && sees_alive(b, &a_key)
|
|
|
|
|
});
|
2026-02-19 14:39:33 +00:00
|
|
|
|
2026-06-23 15:42:28 +00:00
|
|
|
assert!(
|
|
|
|
|
converged,
|
|
|
|
|
"nodes did not converge within timeout (mutual join)"
|
|
|
|
|
);
|
2026-06-09 09:29:07 +00:00
|
|
|
assert_eq!(node_a.alive_count(), 1);
|
|
|
|
|
assert_eq!(node_b.alive_count(), 1);
|
2026-02-19 14:39:33 +00:00
|
|
|
|
|
|
|
|
node_a.shutdown();
|
|
|
|
|
node_b.shutdown();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn two_nodes_form_cluster_with_peer_auth() {
|
|
|
|
|
let auth_a = Arc::new(Mutex::new(PeerAllowList::open()));
|
|
|
|
|
let auth_b = Arc::new(Mutex::new(PeerAllowList::open()));
|
|
|
|
|
|
|
|
|
|
let mut node_a = make_driver_with_auth(auth_a.clone());
|
|
|
|
|
let mut node_b = make_driver_with_auth(auth_b.clone());
|
|
|
|
|
|
|
|
|
|
let a_id = node_a.node_id();
|
|
|
|
|
let b_id = node_b.node_id();
|
|
|
|
|
let b_addr = node_b.endpoint_addr();
|
|
|
|
|
|
|
|
|
|
// Switch to restrictive mode by adding each other
|
|
|
|
|
auth_a.lock().unwrap().add_peer(b_id, "node-b".into());
|
|
|
|
|
auth_b.lock().unwrap().add_peer(a_id, "node-a".into());
|
|
|
|
|
|
|
|
|
|
node_a.join(&[b_addr]);
|
|
|
|
|
|
2026-06-23 15:42:28 +00:00
|
|
|
let converged = pump_until_pair(&mut node_a, &mut node_b, Duration::from_secs(5), |a, b| {
|
|
|
|
|
let a_key = PublicKey::from_bytes(&a.node_id().0).unwrap();
|
|
|
|
|
let b_key = PublicKey::from_bytes(&b.node_id().0).unwrap();
|
|
|
|
|
sees_alive(a, &b_key) && sees_alive(b, &a_key)
|
|
|
|
|
});
|
2026-02-19 14:39:33 +00:00
|
|
|
|
2026-06-23 15:42:28 +00:00
|
|
|
assert!(
|
|
|
|
|
converged,
|
|
|
|
|
"nodes with peer auth did not converge within timeout"
|
|
|
|
|
);
|
2026-06-09 09:29:07 +00:00
|
|
|
assert_eq!(node_a.alive_count(), 1);
|
|
|
|
|
assert_eq!(node_b.alive_count(), 1);
|
2026-02-19 14:39:33 +00:00
|
|
|
|
|
|
|
|
node_a.shutdown();
|
|
|
|
|
node_b.shutdown();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn peer_auth_prevents_unauthorized_join() {
|
|
|
|
|
let mut node_a = make_driver();
|
|
|
|
|
|
|
|
|
|
// Node B has auth with only a dummy peer — node_a is NOT authorized
|
|
|
|
|
let auth_b = Arc::new(Mutex::new(PeerAllowList::open()));
|
|
|
|
|
let dummy_id = distribution::types::NodeId([0xAA; 32]);
|
|
|
|
|
auth_b.lock().unwrap().add_peer(dummy_id, "dummy".into());
|
|
|
|
|
let mut node_b = make_driver_with_auth(auth_b);
|
|
|
|
|
|
|
|
|
|
let b_addr = node_b.endpoint_addr();
|
|
|
|
|
node_a.join(&[b_addr]);
|
|
|
|
|
|
2026-06-23 15:42:28 +00:00
|
|
|
let converged = pump_until_pair(&mut node_a, &mut node_b, Duration::from_secs(3), |_a, b| {
|
|
|
|
|
b.alive_count() > 0
|
|
|
|
|
});
|
2026-02-19 14:39:33 +00:00
|
|
|
|
|
|
|
|
assert!(!converged, "unauthorized peer should NOT have joined");
|
2026-06-09 09:29:07 +00:00
|
|
|
assert_eq!(node_b.alive_count(), 0, "node_b should have no alive peers");
|
2026-02-19 14:39:33 +00:00
|
|
|
|
|
|
|
|
node_a.shutdown();
|
|
|
|
|
node_b.shutdown();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ─── Multi-node tests ───────────────────────────────────────────────────
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn three_nodes_converge_via_star_join() {
|
|
|
|
|
let mut cluster = IrohTestCluster::star(3);
|
|
|
|
|
|
2026-06-09 09:29:07 +00:00
|
|
|
let converged = cluster.pump_until(Duration::from_secs(10), |nodes| {
|
|
|
|
|
nodes.iter().all(|d| d.alive_count() == 2)
|
2026-02-19 14:39:33 +00:00
|
|
|
});
|
|
|
|
|
|
|
|
|
|
assert!(converged, "3-node star did not converge");
|
|
|
|
|
cluster.shutdown();
|
|
|
|
|
}
|
2026-06-09 09:29:07 +00:00
|
|
|
|
|
|
|
|
// ─── Goal 2 (real-QUIC) — genuine death is detected ──────────────────────
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn goal2_shutdown_node_is_detected_dead_by_survivors() {
|
|
|
|
|
// BEHAVIORAL_TEST_SPEC Goal 2 over real QUIC: shut one node down for real and
|
|
|
|
|
// poll until the survivors converge on it being Dead — a genuine probe
|
|
|
|
|
// timeout, not an injected death. Observed only through the membership view.
|
|
|
|
|
let mut cluster = IrohTestCluster::star(3);
|
|
|
|
|
let converged = cluster.pump_until(Duration::from_secs(10), |nodes| {
|
|
|
|
|
nodes.iter().all(|d| d.alive_count() == 2)
|
|
|
|
|
});
|
|
|
|
|
assert!(converged, "precondition: 3-node star must converge");
|
|
|
|
|
|
|
|
|
|
let dead = 2usize;
|
|
|
|
|
let dead_key = cluster.key(dead);
|
|
|
|
|
cluster.shutdown_one(dead);
|
|
|
|
|
|
|
|
|
|
// The survivors' probes to the dead node now truly fail; drive them until
|
|
|
|
|
// both converge on it being Dead.
|
|
|
|
|
let detected = cluster.pump_until_excluding(&[dead], Duration::from_secs(30), |drivers| {
|
|
|
|
|
drivers
|
|
|
|
|
.iter()
|
|
|
|
|
.enumerate()
|
|
|
|
|
.all(|(i, d)| i == dead || sees_dead(d, &dead_key))
|
|
|
|
|
});
|
|
|
|
|
assert!(detected, "survivors must detect the shut-down node as Dead");
|
|
|
|
|
|
|
|
|
|
// Tear down the two survivors (the third is already shut down).
|
|
|
|
|
cluster[0].shutdown();
|
|
|
|
|
cluster[1].shutdown();
|
|
|
|
|
}
|