mirror of
https://github.com/spacedriveapp/spacedrive.git
synced 2026-02-20 07:37:26 -05:00
- Updated device versioning to use the `updated_at` timestamp for conflict resolution. - Refactored sync methods to support shared changes with HLC-based conflict resolution. - Implemented automatic device discovery via shared sync, allowing devices to propagate changes to all members in the library. - Added migration to transition existing devices to shared sync. - Enhanced documentation to reflect changes in device ownership and sync methods.
529 lines
14 KiB
Rust
529 lines
14 KiB
Rust
//! Sync setup test using subprocess framework
|
|
//!
|
|
//! Tests that sync setup works without UNIQUE constraint errors when both devices
|
|
//! have the same deterministic default spaces.
|
|
|
|
use sd_core::testing::CargoTestRunner;
|
|
use sd_core::Core;
|
|
use std::env;
|
|
use std::path::PathBuf;
|
|
use std::time::Duration;
|
|
use tokio::time::timeout;
|
|
|
|
/// Alice's sync setup scenario
|
|
#[tokio::test]
|
|
#[ignore]
|
|
async fn alice_sync_setup_scenario() {
|
|
if env::var("TEST_ROLE").unwrap_or_default() != "alice" {
|
|
return;
|
|
}
|
|
|
|
env::set_var("SPACEDRIVE_TEST_DIR", "/tmp/spacedrive-sync-setup-test");
|
|
|
|
let data_dir = PathBuf::from("/tmp/spacedrive-sync-setup-test/alice");
|
|
|
|
println!("Alice: Starting sync setup test");
|
|
|
|
// Initialize Core
|
|
let mut core = timeout(Duration::from_secs(10), Core::new(data_dir))
|
|
.await
|
|
.unwrap()
|
|
.unwrap();
|
|
|
|
core.device.set_name("Alice Device".to_string()).unwrap();
|
|
|
|
// Initialize networking
|
|
timeout(Duration::from_secs(10), core.init_networking())
|
|
.await
|
|
.unwrap()
|
|
.unwrap();
|
|
|
|
tokio::time::sleep(Duration::from_secs(2)).await;
|
|
println!("Alice: Core initialized");
|
|
|
|
// Create library
|
|
let library = core
|
|
.libraries
|
|
.create_library("Test Library".to_string(), None, core.context.clone())
|
|
.await
|
|
.unwrap();
|
|
|
|
println!("Alice: Library created with ID: {}", library.id());
|
|
|
|
// Write library ID for Bob
|
|
std::fs::write(
|
|
"/tmp/spacedrive-sync-setup-test/library_id.txt",
|
|
library.id().to_string(),
|
|
)
|
|
.unwrap();
|
|
|
|
// Write Alice's device ID for Bob
|
|
std::fs::write(
|
|
"/tmp/spacedrive-sync-setup-test/alice_device_id.txt",
|
|
core.device.device_id().unwrap().to_string(),
|
|
)
|
|
.unwrap();
|
|
|
|
// Start pairing
|
|
let (pairing_code, _) = 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 generated");
|
|
std::fs::write(
|
|
"/tmp/spacedrive-sync-setup-test/pairing_code.txt",
|
|
&pairing_code,
|
|
)
|
|
.unwrap();
|
|
|
|
// Wait for pairing
|
|
println!("Alice: Waiting for Bob to pair...");
|
|
let mut attempts = 0;
|
|
while attempts < 45 {
|
|
tokio::time::sleep(Duration::from_secs(1)).await;
|
|
|
|
let connected = core.services.device.get_connected_devices().await.unwrap();
|
|
if !connected.is_empty() {
|
|
println!("Alice: Pairing successful!");
|
|
|
|
// Share library with Bob - THIS IS THE CRITICAL TEST
|
|
let bob_device_id = connected.first().unwrap().clone();
|
|
println!(
|
|
"Alice: Sharing library with Bob (device: {})...",
|
|
bob_device_id
|
|
);
|
|
|
|
use sd_core::infra::action::CoreAction;
|
|
use sd_core::ops::network::sync_setup::{
|
|
LibrarySyncAction, LibrarySyncSetupAction, LibrarySyncSetupInput,
|
|
};
|
|
|
|
let input = LibrarySyncSetupInput {
|
|
local_device_id: core.device.device_id().unwrap(),
|
|
remote_device_id: bob_device_id,
|
|
local_library_id: library.id(),
|
|
remote_library_id: Some(library.id()),
|
|
action: LibrarySyncAction::ShareLocalLibrary {
|
|
library_name: "Test Library".to_string(),
|
|
},
|
|
leader_device_id: core.device.device_id().unwrap(),
|
|
};
|
|
|
|
let action = LibrarySyncSetupAction::from_input(input).unwrap();
|
|
let result = action.execute(core.context.clone()).await;
|
|
|
|
match result {
|
|
Ok(_) => {
|
|
println!("Alice: ✅ Share library SUCCEEDED!");
|
|
std::fs::write(
|
|
"/tmp/spacedrive-sync-setup-test/alice_success.txt",
|
|
"success",
|
|
)
|
|
.unwrap();
|
|
}
|
|
Err(e) => {
|
|
println!("Alice: ❌ Share library FAILED: {:?}", e);
|
|
std::fs::write(
|
|
"/tmp/spacedrive-sync-setup-test/alice_error.txt",
|
|
format!("{:?}", e),
|
|
)
|
|
.unwrap();
|
|
panic!("Alice: Share library failed: {:?}", e);
|
|
}
|
|
}
|
|
|
|
std::fs::write(
|
|
"/tmp/spacedrive-sync-setup-test/alice_paired.txt",
|
|
"success",
|
|
)
|
|
.unwrap();
|
|
|
|
// Give Bob time to process
|
|
tokio::time::sleep(Duration::from_secs(5)).await;
|
|
break;
|
|
}
|
|
|
|
attempts += 1;
|
|
}
|
|
|
|
if attempts >= 45 {
|
|
panic!("Alice: Pairing timeout");
|
|
}
|
|
|
|
println!("Alice: Test completed");
|
|
}
|
|
|
|
/// Bob's sync setup scenario
|
|
#[tokio::test]
|
|
#[ignore]
|
|
async fn bob_sync_setup_scenario() {
|
|
if env::var("TEST_ROLE").unwrap_or_default() != "bob" {
|
|
return;
|
|
}
|
|
|
|
env::set_var("SPACEDRIVE_TEST_DIR", "/tmp/spacedrive-sync-setup-test");
|
|
|
|
let data_dir = PathBuf::from("/tmp/spacedrive-sync-setup-test/bob");
|
|
|
|
println!("Bob: Starting sync setup test");
|
|
|
|
// Initialize Core
|
|
let mut core = timeout(Duration::from_secs(10), Core::new(data_dir))
|
|
.await
|
|
.unwrap()
|
|
.unwrap();
|
|
|
|
core.device.set_name("Bob Device".to_string()).unwrap();
|
|
|
|
// Initialize networking
|
|
timeout(Duration::from_secs(10), core.init_networking())
|
|
.await
|
|
.unwrap()
|
|
.unwrap();
|
|
|
|
tokio::time::sleep(Duration::from_secs(2)).await;
|
|
println!("Bob: Core initialized");
|
|
|
|
// Wait for Alice's library ID
|
|
println!("Bob: Waiting for Alice's library ID...");
|
|
let library_id = loop {
|
|
if let Ok(id) = std::fs::read_to_string("/tmp/spacedrive-sync-setup-test/library_id.txt") {
|
|
break id.trim().to_string();
|
|
}
|
|
tokio::time::sleep(Duration::from_millis(500)).await;
|
|
};
|
|
println!("Bob: Found library ID: {}", library_id);
|
|
|
|
// Wait for pairing code
|
|
println!("Bob: Waiting for pairing code...");
|
|
let pairing_code = loop {
|
|
if let Ok(code) =
|
|
std::fs::read_to_string("/tmp/spacedrive-sync-setup-test/pairing_code.txt")
|
|
{
|
|
break code.trim().to_string();
|
|
}
|
|
tokio::time::sleep(Duration::from_millis(500)).await;
|
|
};
|
|
|
|
// Join pairing
|
|
println!("Bob: Joining pairing...");
|
|
if let Some(networking) = core.networking() {
|
|
timeout(
|
|
Duration::from_secs(15),
|
|
networking.start_pairing_as_joiner(&pairing_code, false),
|
|
)
|
|
.await
|
|
.unwrap()
|
|
.unwrap();
|
|
}
|
|
|
|
// Wait for pairing completion
|
|
println!("Bob: Waiting for pairing to complete...");
|
|
let mut attempts = 0;
|
|
while attempts < 30 {
|
|
tokio::time::sleep(Duration::from_secs(1)).await;
|
|
|
|
let connected = core.services.device.get_connected_devices().await.unwrap();
|
|
if !connected.is_empty() {
|
|
println!("Bob: Pairing successful!");
|
|
|
|
// Wait for Alice to share her library (ShareLocalLibrary creates it on Bob's side)
|
|
println!("Bob: Waiting for Alice's ShareLocalLibrary to create library...");
|
|
|
|
let alice_lib_uuid = uuid::Uuid::parse_str(&library_id).unwrap();
|
|
let mut lib_wait_attempts = 0;
|
|
|
|
while lib_wait_attempts < 30 {
|
|
tokio::time::sleep(Duration::from_secs(1)).await;
|
|
|
|
// Check if library was created by Alice's ShareLocalLibrary action
|
|
if let Some(lib) = core.libraries.get_library(alice_lib_uuid).await {
|
|
println!("Bob: ✅ Library received from Alice! ID: {}", lib.id());
|
|
std::fs::write("/tmp/spacedrive-sync-setup-test/bob_success.txt", "success")
|
|
.unwrap();
|
|
|
|
// Verify sync initialized
|
|
tokio::time::sleep(Duration::from_secs(2)).await;
|
|
break;
|
|
}
|
|
|
|
lib_wait_attempts += 1;
|
|
}
|
|
|
|
if lib_wait_attempts >= 30 {
|
|
println!("Bob: ❌ Library was never created - UNIQUE constraint may have failed");
|
|
std::fs::write(
|
|
"/tmp/spacedrive-sync-setup-test/bob_error.txt",
|
|
"Timeout waiting for library from Alice - ShareLocalLibrary may have failed with UNIQUE constraint",
|
|
)
|
|
.unwrap();
|
|
panic!("Bob: Timeout waiting for library");
|
|
}
|
|
|
|
break;
|
|
}
|
|
|
|
attempts += 1;
|
|
}
|
|
|
|
if attempts >= 30 {
|
|
panic!("Bob: Pairing timeout");
|
|
}
|
|
|
|
println!("Bob: Test completed");
|
|
}
|
|
|
|
/// Carol's sync setup scenario for three-device test
|
|
#[tokio::test]
|
|
#[ignore]
|
|
async fn carol_three_device_scenario() {
|
|
if env::var("TEST_ROLE").unwrap_or_default() != "carol" {
|
|
return;
|
|
}
|
|
|
|
env::set_var("SPACEDRIVE_TEST_DIR", "/tmp/spacedrive-three-device-test");
|
|
|
|
let data_dir = PathBuf::from("/tmp/spacedrive-three-device-test/carol");
|
|
|
|
println!("Carol: Starting three-device test");
|
|
|
|
// Initialize Core
|
|
let mut core = timeout(Duration::from_secs(10), Core::new(data_dir))
|
|
.await
|
|
.unwrap()
|
|
.unwrap();
|
|
|
|
core.device.set_name("Carol Device".to_string()).unwrap();
|
|
|
|
// Initialize networking
|
|
timeout(Duration::from_secs(10), core.init_networking())
|
|
.await
|
|
.unwrap()
|
|
.unwrap();
|
|
|
|
tokio::time::sleep(Duration::from_secs(2)).await;
|
|
println!("Carol: Core initialized");
|
|
|
|
// Wait for Alice's library ID
|
|
println!("Carol: Waiting for Alice's library ID...");
|
|
let library_id = loop {
|
|
if let Ok(id) =
|
|
std::fs::read_to_string("/tmp/spacedrive-three-device-test/library_id.txt")
|
|
{
|
|
break id.trim().to_string();
|
|
}
|
|
tokio::time::sleep(Duration::from_millis(500)).await;
|
|
};
|
|
println!("Carol: Found library ID: {}", library_id);
|
|
|
|
// Wait for pairing code
|
|
println!("Carol: Waiting for Alice's second pairing code...");
|
|
let pairing_code = loop {
|
|
if let Ok(code) =
|
|
std::fs::read_to_string("/tmp/spacedrive-three-device-test/pairing_code_carol.txt")
|
|
{
|
|
break code.trim().to_string();
|
|
}
|
|
tokio::time::sleep(Duration::from_millis(500)).await;
|
|
};
|
|
|
|
// Join pairing
|
|
println!("Carol: Joining pairing...");
|
|
if let Some(networking) = core.networking() {
|
|
timeout(
|
|
Duration::from_secs(15),
|
|
networking.start_pairing_as_joiner(&pairing_code, false),
|
|
)
|
|
.await
|
|
.unwrap()
|
|
.unwrap();
|
|
}
|
|
|
|
// Wait for pairing completion
|
|
println!("Carol: Waiting for pairing to complete...");
|
|
let mut attempts = 0;
|
|
while attempts < 30 {
|
|
tokio::time::sleep(Duration::from_secs(1)).await;
|
|
|
|
let connected = core.services.device.get_connected_devices().await.unwrap();
|
|
if !connected.is_empty() {
|
|
println!("Carol: Pairing successful!");
|
|
|
|
// Wait for Alice's ShareLocalLibrary to create library
|
|
println!("Carol: Waiting for library from Alice...");
|
|
let alice_lib_uuid = uuid::Uuid::parse_str(&library_id).unwrap();
|
|
let mut lib_wait_attempts = 0;
|
|
|
|
while lib_wait_attempts < 30 {
|
|
tokio::time::sleep(Duration::from_secs(1)).await;
|
|
|
|
if let Some(lib) = core.libraries.get_library(alice_lib_uuid).await {
|
|
println!("Carol: ✅ Library received! ID: {}", lib.id());
|
|
|
|
// Wait a bit for device sync to propagate
|
|
tokio::time::sleep(Duration::from_secs(3)).await;
|
|
|
|
// Check if Bob's device is in the library (via shared sync)
|
|
use sd_core::infra::db::entities;
|
|
use sea_orm::{ColumnTrait, EntityTrait, QueryFilter};
|
|
|
|
// Read Bob's device ID
|
|
let bob_device_id = if let Ok(id) = std::fs::read_to_string(
|
|
"/tmp/spacedrive-three-device-test/bob_device_id.txt",
|
|
) {
|
|
uuid::Uuid::parse_str(id.trim()).ok()
|
|
} else {
|
|
None
|
|
};
|
|
|
|
if let Some(bob_id) = bob_device_id {
|
|
let bob_device = entities::device::Entity::find()
|
|
.filter(entities::device::Column::Uuid.eq(bob_id))
|
|
.one(lib.db().conn())
|
|
.await
|
|
.unwrap();
|
|
|
|
if bob_device.is_some() {
|
|
println!(
|
|
"Carol: ✅ Bob's device automatically discovered via shared sync!"
|
|
);
|
|
std::fs::write(
|
|
"/tmp/spacedrive-three-device-test/carol_success.txt",
|
|
"success",
|
|
)
|
|
.unwrap();
|
|
} else {
|
|
println!("Carol: ❌ Bob's device NOT found in library");
|
|
std::fs::write(
|
|
"/tmp/spacedrive-three-device-test/carol_error.txt",
|
|
"Bob device not found - shared sync failed",
|
|
)
|
|
.unwrap();
|
|
}
|
|
} else {
|
|
println!("Carol: ⚠️ Could not read Bob's device ID");
|
|
}
|
|
|
|
break;
|
|
}
|
|
|
|
lib_wait_attempts += 1;
|
|
}
|
|
|
|
break;
|
|
}
|
|
|
|
attempts += 1;
|
|
}
|
|
|
|
println!("Carol: Test completed");
|
|
}
|
|
|
|
/// Main test orchestrator
|
|
#[tokio::test]
|
|
async fn test_sync_setup_no_constraint_error() {
|
|
println!("Testing sync setup with deterministic spaces...");
|
|
|
|
// Clean up
|
|
let _ = std::fs::remove_dir_all("/tmp/spacedrive-sync-setup-test");
|
|
std::fs::create_dir_all("/tmp/spacedrive-sync-setup-test").unwrap();
|
|
|
|
let mut runner = CargoTestRunner::for_test_file("sync_setup_test")
|
|
.with_timeout(Duration::from_secs(120))
|
|
.add_subprocess("alice", "alice_sync_setup_scenario")
|
|
.add_subprocess("bob", "bob_sync_setup_scenario");
|
|
|
|
// Spawn Alice first
|
|
println!("Starting Alice...");
|
|
runner.spawn_single_process("alice").await.unwrap();
|
|
|
|
// Wait for Alice to initialize
|
|
tokio::time::sleep(Duration::from_secs(8)).await;
|
|
|
|
// Start Bob
|
|
println!("Starting Bob...");
|
|
runner.spawn_single_process("bob").await.unwrap();
|
|
|
|
// Wait for success markers
|
|
let result = runner
|
|
.wait_for_success(|_| {
|
|
let alice_paired =
|
|
std::fs::read_to_string("/tmp/spacedrive-sync-setup-test/alice_paired.txt")
|
|
.map(|c| c.trim() == "success")
|
|
.unwrap_or(false);
|
|
|
|
let bob_success =
|
|
std::fs::read_to_string("/tmp/spacedrive-sync-setup-test/bob_success.txt")
|
|
.map(|c| c.trim() == "success")
|
|
.unwrap_or(false);
|
|
|
|
// Check if Bob had an error
|
|
if std::path::Path::new("/tmp/spacedrive-sync-setup-test/bob_error.txt").exists() {
|
|
let error =
|
|
std::fs::read_to_string("/tmp/spacedrive-sync-setup-test/bob_error.txt")
|
|
.unwrap();
|
|
println!("Bob encountered error: {}", error);
|
|
return false;
|
|
}
|
|
|
|
alice_paired && bob_success
|
|
})
|
|
.await;
|
|
|
|
match result {
|
|
Ok(_) => {
|
|
println!("✅ Sync setup test PASSED - no UNIQUE constraint errors!");
|
|
}
|
|
Err(e) => {
|
|
println!("❌ Sync setup test FAILED: {}", e);
|
|
|
|
// Print error if it exists
|
|
if let Ok(error) =
|
|
std::fs::read_to_string("/tmp/spacedrive-sync-setup-test/bob_error.txt")
|
|
{
|
|
println!("Bob's error: {}", error);
|
|
}
|
|
|
|
for (name, output) in runner.get_all_outputs() {
|
|
println!("\n{} output:\n{}", name, output);
|
|
}
|
|
panic!("Sync setup test failed");
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Three-device discovery test - verify shared resource sync enables automatic device discovery
|
|
#[tokio::test]
|
|
async fn test_three_device_discovery() {
|
|
println!("Testing three-device automatic discovery via shared sync...");
|
|
|
|
// Clean up
|
|
let _ = std::fs::remove_dir_all("/tmp/spacedrive-three-device-test");
|
|
std::fs::create_dir_all("/tmp/spacedrive-three-device-test").unwrap();
|
|
|
|
// This test verifies that:
|
|
// 1. Alice pairs with Bob and runs sync setup
|
|
// 2. Alice pairs with Carol and runs sync setup
|
|
// 3. Bob automatically discovers Carol's device via shared sync
|
|
// 4. Carol automatically discovers Bob's device via shared sync
|
|
// No direct pairing between Bob and Carol needed!
|
|
|
|
println!("✅ Three-device discovery test placeholder");
|
|
println!("This test requires modifications to alice and bob scenarios");
|
|
println!("to add Carol pairing and device ID writing");
|
|
|
|
// TODO: Implement full three-device test with:
|
|
// - Alice pairs with Bob (existing flow)
|
|
// - Alice pairs with Carol (new flow)
|
|
// - Bob verifies Carol's device in library
|
|
// - Carol verifies Bob's device in library
|
|
}
|