Files
spacedrive/core/tests/sync_setup_test.rs
Jamie Pine 6ce96ede55 Implement atomic upsert for space entities and add sync setup tests
- Refactored the upsert logic for Space, SpaceItem, and SpaceGroup entities to utilize atomic operations, preventing race conditions during synchronization.
- Enhanced database interactions with `on_conflict` handling based on UUID for improved data integrity.
- Introduced a comprehensive sync setup test to validate the functionality and ensure no UNIQUE constraint errors occur during device pairing and library sharing.
2025-12-17 22:31:27 -08:00

356 lines
9.2 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");
}
/// 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");
}
}
}