Files
spacedrive/core/tests/proxy_pairing_test.rs
Jamie Pine 388b4dde28 cargo fmt
2026-01-24 16:14:21 -08:00

649 lines
19 KiB
Rust

//! Proxy pairing test using the cargo test subprocess framework
//!
//! This is a STRICT test that demonstrates proxy/vouching-based pairing:
//! 1. Alice pairs with Carol (direct pairing) - VERIFIED
//! 2. Alice pairs with Bob (direct pairing) - VERIFIED
//! 3. Alice auto-vouches Bob to Carol (proxy pairing) - VERIFIED
//! 4. Carol auto-accepts the vouch - VERIFIED
//! 5. Bob receives ProxyPairingComplete - VERIFIED
//!
//! The test FAILS if:
//! - Bob does not appear in Carol's paired devices list (Carol panics)
//! - Carol does not appear in Bob's paired devices list (Bob panics)
//! - Any device times out waiting for proxy pairing (30 second limit)
//!
//! Config: auto_vouch_to_all=true, auto_accept_vouched=true
use sd_core::testing::CargoTestRunner;
use sd_core::Core;
use std::env;
use std::path::PathBuf;
use std::time::Duration;
use tokio::time::timeout;
const TEST_DIR: &str = "/tmp/spacedrive-proxy-pairing-test";
/// Alice's scenario - pairs with Carol first, then Bob, then vouches Bob to Carol
#[tokio::test]
#[ignore]
async fn alice_proxy_pairing_scenario() {
if env::var("TEST_ROLE").unwrap_or_default() != "alice" {
return;
}
env::set_var("SPACEDRIVE_TEST_DIR", TEST_DIR);
let data_dir = PathBuf::from(format!("{}/alice", TEST_DIR));
let device_name = "Alice's Test Device";
println!("Alice: Starting proxy pairing test");
println!("Alice: Data dir: {:?}", data_dir);
// Initialize Core
println!("Alice: Initializing Core...");
let mut core = timeout(Duration::from_secs(10), Core::new(data_dir))
.await
.unwrap()
.unwrap();
println!("Alice: Core initialized");
core.device.set_name(device_name.to_string()).unwrap();
// Enable auto-vouch for testing
println!("Alice: Enabling auto-vouch for testing...");
{
let mut config = core.config.write().await;
config.proxy_pairing.auto_vouch_to_all = true;
config.save().unwrap();
}
println!("Alice: Auto-vouch enabled");
// Initialize networking
println!("Alice: Initializing networking...");
timeout(Duration::from_secs(10), core.init_networking())
.await
.unwrap()
.unwrap();
tokio::time::sleep(Duration::from_secs(3)).await;
println!("Alice: Networking initialized");
// Phase 1: Pair with Carol
println!("\n=== PHASE 1: Alice pairs with Carol ===");
let (pairing_code_carol, _) = if let Some(networking) = core.networking() {
timeout(
Duration::from_secs(15),
networking.start_pairing_as_initiator(false),
)
.await
.unwrap()
.unwrap()
} else {
panic!("Networking not initialized");
};
println!("Alice: Pairing code for Carol: {}", pairing_code_carol);
std::fs::write(
format!("{}/pairing_code_carol.txt", TEST_DIR),
&pairing_code_carol,
)
.unwrap();
println!("Alice: Waiting for Carol to pair...");
// Wait for Carol to pair
let mut attempts = 0;
loop {
tokio::time::sleep(Duration::from_secs(1)).await;
let paired_devices = if let Some(networking) = core.networking() {
networking
.device_registry()
.read()
.await
.get_paired_devices()
} else {
vec![]
};
if !paired_devices.is_empty() {
println!("Alice: Carol paired successfully!");
for device in &paired_devices {
println!(
"Alice sees: {} (ID: {})",
device.device_name, device.device_id
);
}
break;
}
attempts += 1;
if attempts >= 45 {
panic!("Alice: Timeout waiting for Carol");
}
}
// Signal Carol pairing complete
std::fs::write(format!("{}/alice_carol_paired.txt", TEST_DIR), "success").unwrap();
// Phase 2: Pair with Bob
println!("\n=== PHASE 2: Alice pairs with Bob ===");
tokio::time::sleep(Duration::from_secs(2)).await;
let (pairing_code_bob, _) = if let Some(networking) = core.networking() {
timeout(
Duration::from_secs(15),
networking.start_pairing_as_initiator(false),
)
.await
.unwrap()
.unwrap()
} else {
panic!("Networking not initialized");
};
println!("Alice: Pairing code for Bob: {}", pairing_code_bob);
std::fs::write(
format!("{}/pairing_code_bob.txt", TEST_DIR),
&pairing_code_bob,
)
.unwrap();
println!("Alice: Waiting for Bob to pair...");
// Wait for Bob to pair
attempts = 0;
let mut bob_device_id = None;
loop {
tokio::time::sleep(Duration::from_secs(1)).await;
let paired_devices = if let Some(networking) = core.networking() {
networking
.device_registry()
.read()
.await
.get_paired_devices()
} else {
vec![]
};
if paired_devices.len() >= 2 {
println!("Alice: Bob paired successfully!");
for device in &paired_devices {
println!(
"Alice sees: {} (ID: {})",
device.device_name, device.device_id
);
if device.device_name == "Bob's Test Device" {
bob_device_id = Some(device.device_id);
}
}
break;
}
attempts += 1;
if attempts >= 45 {
panic!("Alice: Timeout waiting for Bob");
}
}
let bob_id = bob_device_id.expect("Bob's device ID not found");
println!("Alice: Bob's device ID: {}", bob_id);
// Signal Bob pairing complete
std::fs::write(format!("{}/alice_bob_paired.txt", TEST_DIR), "success").unwrap();
// Phase 3: Vouch Bob to Carol
println!("\n=== PHASE 3: Alice vouches Bob to Carol ===");
tokio::time::sleep(Duration::from_secs(2)).await;
// Get Carol's device ID
let paired_devices = if let Some(networking) = core.networking() {
networking
.device_registry()
.read()
.await
.get_paired_devices()
} else {
vec![]
};
let carol_id = paired_devices
.iter()
.find(|d| d.device_name == "Carol's Test Device")
.map(|d| d.device_id)
.expect("Carol not found");
println!(
"Alice: Vouching Bob (ID: {}) to Carol (ID: {})",
bob_id, carol_id
);
// With auto_vouch_to_all enabled, Alice should automatically send
// ProxyPairingRequest to Carol after pairing with Bob
println!("Alice: Auto-vouch enabled - ProxyPairingRequest should be sent to Carol");
println!("Alice: Carol should auto-accept and pair with Bob via proxy");
// Write marker that vouching is ready
std::fs::write(format!("{}/alice_vouching_ready.txt", TEST_DIR), "success").unwrap();
// Wait for vouching to complete
println!("Alice: Waiting for vouching to complete...");
tokio::time::sleep(Duration::from_secs(15)).await;
// Write success marker
std::fs::write(format!("{}/alice_success.txt", TEST_DIR), "success").unwrap();
println!("Alice: Test completed successfully");
// Keep Alice alive for a bit longer
tokio::time::sleep(Duration::from_secs(5)).await;
}
/// Carol's scenario - pairs with Alice first, then accepts vouch for Bob
#[tokio::test]
#[ignore]
async fn carol_proxy_pairing_scenario() {
if env::var("TEST_ROLE").unwrap_or_default() != "carol" {
return;
}
env::set_var("SPACEDRIVE_TEST_DIR", TEST_DIR);
let data_dir = PathBuf::from(format!("{}/carol", TEST_DIR));
let device_name = "Carol's Test Device";
println!("Carol: Starting proxy pairing test");
println!("Carol: Data dir: {:?}", data_dir);
// Initialize Core
println!("Carol: Initializing Core...");
let mut core = timeout(Duration::from_secs(10), Core::new(data_dir))
.await
.unwrap()
.unwrap();
println!("Carol: Core initialized");
core.device.set_name(device_name.to_string()).unwrap();
// Verify auto-accept is enabled (it's true by default)
println!("Carol: Auto-accept proxy pairing enabled (default)");
// Initialize networking
println!("Carol: Initializing networking...");
timeout(Duration::from_secs(10), core.init_networking())
.await
.unwrap()
.unwrap();
tokio::time::sleep(Duration::from_secs(3)).await;
println!("Carol: Networking initialized");
// Phase 1: Pair with Alice
println!("\n=== PHASE 1: Carol pairs with Alice ===");
println!("Carol: Waiting for pairing code from Alice...");
let pairing_code = loop {
if let Ok(code) = std::fs::read_to_string(format!("{}/pairing_code_carol.txt", TEST_DIR)) {
break code.trim().to_string();
}
tokio::time::sleep(Duration::from_millis(500)).await;
};
println!("Carol: Found pairing code");
// Join pairing with Alice
if let Some(networking) = core.networking() {
timeout(
Duration::from_secs(15),
networking.start_pairing_as_joiner(&pairing_code, false),
)
.await
.unwrap()
.unwrap();
}
println!("Carol: Joined pairing with Alice");
// Wait for pairing completion
let mut attempts = 0;
loop {
tokio::time::sleep(Duration::from_secs(1)).await;
let paired_devices = if let Some(networking) = core.networking() {
networking
.device_registry()
.read()
.await
.get_paired_devices()
} else {
vec![]
};
if !paired_devices.is_empty() {
println!("Carol: Paired with Alice successfully!");
for device in &paired_devices {
println!(
"Carol sees: {} (ID: {})",
device.device_name, device.device_id
);
}
break;
}
attempts += 1;
if attempts >= 30 {
panic!("Carol: Timeout pairing with Alice");
}
}
tokio::time::sleep(Duration::from_secs(5)).await;
// Phase 2: Wait for proxy pairing from Alice vouching for Bob
println!("\n=== PHASE 2: Carol receives proxy pairing for Bob ===");
println!("Carol: Waiting for Alice to vouch Bob...");
// Wait for vouching to be ready
loop {
if std::fs::read_to_string(format!("{}/alice_vouching_ready.txt", TEST_DIR)).is_ok() {
break;
}
tokio::time::sleep(Duration::from_millis(500)).await;
}
println!("Carol: Alice has initiated vouching");
// Carol should receive ProxyPairingRequest from Alice
// With auto_accept_vouched=true, Carol will automatically accept and pair with Bob
println!("Carol: Should receive ProxyPairingRequest and auto-accept");
println!("Carol: Waiting for Bob to appear in paired devices...");
// Wait for Bob to be paired via proxy
attempts = 0;
let mut bob_found = false;
loop {
tokio::time::sleep(Duration::from_secs(1)).await;
let paired_devices = if let Some(networking) = core.networking() {
networking
.device_registry()
.read()
.await
.get_paired_devices()
} else {
vec![]
};
// Look for Bob in paired devices
for device in &paired_devices {
if device.device_name == "Bob's Test Device" {
println!("Carol: ✅ Bob found via proxy! (ID: {})", device.device_id);
println!("Carol: Proxy pairing verified - Bob is in paired devices list");
bob_found = true;
break;
}
}
if bob_found {
break;
}
attempts += 1;
if attempts >= 30 {
println!("\n❌ CAROL TEST FAILURE");
println!("Bob was NOT found in Carol's paired devices after 30 seconds");
println!("\nActual: {} device(s) found:", paired_devices.len());
for device in &paired_devices {
println!(" - {} ({})", device.device_name, device.device_id);
}
panic!("Carol: FAILED - Bob not found via proxy pairing");
}
if attempts % 5 == 0 {
println!("Carol: Waiting for Bob proxy pairing... ({}/30)", attempts);
}
}
// Only write success if Bob was actually found via proxy
if !bob_found {
panic!("Carol: FAILED - Proxy pairing check failed");
}
println!("Carol: ✅ Proxy pairing SUCCESS - Bob paired via vouching!");
std::fs::write(format!("{}/carol_success.txt", TEST_DIR), "success").unwrap();
println!("Carol: Test completed");
tokio::time::sleep(Duration::from_secs(5)).await;
}
/// Bob's scenario - pairs with Alice, then gets vouched to Carol
#[tokio::test]
#[ignore]
async fn bob_proxy_pairing_scenario() {
if env::var("TEST_ROLE").unwrap_or_default() != "bob" {
return;
}
env::set_var("SPACEDRIVE_TEST_DIR", TEST_DIR);
let data_dir = PathBuf::from(format!("{}/bob", TEST_DIR));
let device_name = "Bob's Test Device";
println!("Bob: Starting proxy pairing test");
println!("Bob: Data dir: {:?}", data_dir);
// Initialize Core
println!("Bob: Initializing Core...");
let mut core = timeout(Duration::from_secs(10), Core::new(data_dir))
.await
.unwrap()
.unwrap();
println!("Bob: Core initialized");
core.device.set_name(device_name.to_string()).unwrap();
// Initialize networking
println!("Bob: Initializing networking...");
timeout(Duration::from_secs(10), core.init_networking())
.await
.unwrap()
.unwrap();
tokio::time::sleep(Duration::from_secs(3)).await;
println!("Bob: Networking initialized");
// Wait for Alice to pair with Carol first
println!("\n=== PHASE 1: Bob waits for Alice-Carol pairing ===");
println!("Bob: Waiting for Alice to pair with Carol...");
loop {
if std::fs::read_to_string(format!("{}/alice_carol_paired.txt", TEST_DIR)).is_ok() {
break;
}
tokio::time::sleep(Duration::from_millis(500)).await;
}
println!("Bob: Alice and Carol are paired");
// Phase 2: Pair with Alice
println!("\n=== PHASE 2: Bob pairs with Alice ===");
println!("Bob: Waiting for pairing code from Alice...");
let pairing_code = loop {
if let Ok(code) = std::fs::read_to_string(format!("{}/pairing_code_bob.txt", TEST_DIR)) {
break code.trim().to_string();
}
tokio::time::sleep(Duration::from_millis(500)).await;
};
println!("Bob: Found pairing code");
// Join pairing with Alice
if let Some(networking) = core.networking() {
timeout(
Duration::from_secs(15),
networking.start_pairing_as_joiner(&pairing_code, false),
)
.await
.unwrap()
.unwrap();
}
println!("Bob: Joined pairing with Alice");
// Wait for pairing completion
let mut attempts = 0;
loop {
tokio::time::sleep(Duration::from_secs(1)).await;
let paired_devices = if let Some(networking) = core.networking() {
networking
.device_registry()
.read()
.await
.get_paired_devices()
} else {
vec![]
};
if !paired_devices.is_empty() {
println!("Bob: Paired with Alice successfully!");
for device in &paired_devices {
println!(
"Bob sees: {} (ID: {})",
device.device_name, device.device_id
);
}
break;
}
attempts += 1;
if attempts >= 30 {
panic!("Bob: Timeout pairing with Alice");
}
}
tokio::time::sleep(Duration::from_secs(5)).await;
// Phase 3: Wait to be vouched to Carol
println!("\n=== PHASE 3: Bob receives proxy pairing confirmation ===");
println!("Bob: Waiting for Alice to vouch to Carol...");
// Bob should receive ProxyPairingComplete message from Alice
// containing Carol's acceptance, then store Carol as a proxied paired device
println!("Bob: Should receive ProxyPairingComplete with Carol's acceptance");
println!("Bob: Waiting for Carol to appear in paired devices...");
// STRICT CHECK: Carol MUST appear in Bob's paired devices via proxy
attempts = 0;
let mut carol_found = false;
println!("Bob: STRICT CHECK - Carol must appear in paired devices within 30 seconds");
loop {
tokio::time::sleep(Duration::from_secs(1)).await;
let paired_devices = if let Some(networking) = core.networking() {
networking
.device_registry()
.read()
.await
.get_paired_devices()
} else {
vec![]
};
// Look for Carol in paired devices
for device in &paired_devices {
if device.device_name == "Carol's Test Device" {
println!("Bob: ✅ Carol found via proxy! (ID: {})", device.device_id);
println!("Bob: Proxy pairing verified - Carol is in paired devices list");
carol_found = true;
break;
}
}
if carol_found {
break;
}
attempts += 1;
if attempts >= 30 {
println!("\n❌ BOB TEST FAILURE");
println!("Bob: Carol was NOT found in paired devices after 30 seconds");
println!("Bob: This means Bob did not receive ProxyPairingComplete");
println!("Bob: Expected: Carol to appear in paired devices");
println!("Bob: Actual: {} device(s) found", paired_devices.len());
for device in &paired_devices {
println!(" - {} ({})", device.device_name, device.device_id);
}
println!("\nDEBUG: Check logs above for:");
println!(" 1. Alice sending ProxyPairingComplete to Bob");
println!(" 2. Bob receiving and processing the complete message");
println!(" 3. Bob storing Carol as proxied paired device");
panic!("Bob: FAILED - Carol not found via proxy pairing");
}
if attempts % 5 == 0 {
println!("Bob: Waiting for Carol proxy pairing... ({}/30)", attempts);
}
}
// Only write success if Carol was actually found via proxy
if !carol_found {
panic!("Bob: FAILED - Proxy pairing check failed");
}
println!("Bob: ✅ Proxy pairing SUCCESS - Carol paired via vouching!");
std::fs::write(format!("{}/bob_success.txt", TEST_DIR), "success").unwrap();
println!("Bob: Test completed");
tokio::time::sleep(Duration::from_secs(5)).await;
}
/// Main test orchestrator - spawns three devices
#[tokio::test]
async fn test_proxy_pairing() {
println!("Testing proxy/vouching pairing with three devices");
// Clean up test directory
let _ = std::fs::remove_dir_all(TEST_DIR);
std::fs::create_dir_all(TEST_DIR).unwrap();
let mut runner = CargoTestRunner::for_test_file("proxy_pairing_test")
.with_timeout(Duration::from_secs(300))
.add_subprocess("alice", "alice_proxy_pairing_scenario")
.add_subprocess("carol", "carol_proxy_pairing_scenario")
.add_subprocess("bob", "bob_proxy_pairing_scenario");
// Start Alice first (she initiates both pairings)
println!("Starting Alice as initiator...");
runner
.spawn_single_process("alice")
.await
.expect("Failed to spawn Alice");
// Wait for Alice to initialize and generate first pairing code
tokio::time::sleep(Duration::from_secs(8)).await;
// Start Carol (pairs with Alice first)
println!("Starting Carol as first joiner...");
runner
.spawn_single_process("carol")
.await
.expect("Failed to spawn Carol");
// Wait for Alice-Carol pairing to complete
tokio::time::sleep(Duration::from_secs(15)).await;
// Start Bob (pairs with Alice second, gets vouched to Carol)
println!("Starting Bob as second joiner...");
runner
.spawn_single_process("bob")
.await
.expect("Failed to spawn Bob");
// Wait for all phases to complete
let result = runner
.wait_for_success(|_outputs| {
let alice_success = std::fs::read_to_string(format!("{}/alice_success.txt", TEST_DIR))
.map(|content| content.trim() == "success")
.unwrap_or(false);
let carol_success = std::fs::read_to_string(format!("{}/carol_success.txt", TEST_DIR))
.map(|content| content.trim() == "success")
.unwrap_or(false);
let bob_success = std::fs::read_to_string(format!("{}/bob_success.txt", TEST_DIR))
.map(|content| content.trim() == "success")
.unwrap_or(false);
alice_success && carol_success && bob_success
})
.await;
match result {
Ok(_) => {
println!("\n✅ PROXY PAIRING TEST PASSED!");
println!(" ✅ Alice paired with Carol (direct)");
println!(" ✅ Alice paired with Bob (direct)");
println!(" ✅ Alice auto-vouched Bob to Carol");
println!(" ✅ Carol received and accepted Bob's vouch");
println!(" ✅ Bob received ProxyPairingComplete");
println!(" ✅ Bob and Carol are now paired via proxy vouching");
}
Err(e) => {
println!("\n❌ PROXY PAIRING TEST FAILED: {}", e);
println!("\nThis means the vouching protocol did not complete successfully.");
println!("Check the logs above for where the protocol stopped.");
for (name, output) in runner.get_all_outputs() {
println!("\n=== {} OUTPUT ===\n{}", name.to_uppercase(), output);
}
panic!("Proxy pairing test failed - vouching protocol did not work");
}
}
}