#![cfg(all(feature = "db-redb", feature = "iroh"))]
use std::path::{Path, PathBuf};
const CHILD_DIR_ENV: &str = "ATOMIC_DURABILITY_CHILD_DIR";
async fn open_store(dir: &Path) -> atomic_lib::Db {
atomic_lib::Db::init_redb_file(dir, None, &dir.join("uploads"))
.await
.expect("open store")
}
fn scratch_dir(name: &str) -> PathBuf {
let dir =
std::env::temp_dir().join(format!("atomic-durability-{}-{}", name, std::process::id()));
let _ = std::fs::remove_dir_all(&dir);
std::fs::create_dir_all(&dir).unwrap();
dir
}
fn run_child(test_name: &str, dir: &Path) {
let exe = std::env::current_exe().expect("test binary path");
let output = std::process::Command::new(exe)
.args([test_name, "--exact", "--ignored", "--test-threads=1"])
.env(CHILD_DIR_ENV, dir)
.output()
.expect("spawn child");
assert!(
!output.status.success(),
"child was supposed to die uncleanly, but exited normally — the test \
is no longer simulating a kill"
);
}
fn child_dir() -> Option<PathBuf> {
std::env::var(CHILD_DIR_ENV).ok().map(PathBuf::from)
}
#[test]
#[ignore = "child process entry point, driven by the parent test"]
fn child_starts_a_node_then_dies() {
let Some(dir) = child_dir() else { return };
let runtime = tokio::runtime::Runtime::new().unwrap();
runtime.block_on(async {
let store = open_store(&dir).await;
let _ = store.setup("Durability device").await;
let (node_id, _router) = atomic_lib::sync::peer::start(store)
.await
.expect("start peer");
std::fs::write(dir.join("node-id.txt"), node_id.to_string()).unwrap();
});
std::process::abort();
}
#[tokio::test]
async fn the_node_identity_survives_an_unclean_kill() {
let dir = scratch_dir("identity");
run_child("child_starts_a_node_then_dies", &dir);
let before = std::fs::read_to_string(dir.join("node-id.txt"))
.expect("child should have recorded its node id before dying");
let store = open_store(&dir).await;
let (after, _router) = atomic_lib::sync::peer::start(store)
.await
.expect("start peer after restart");
assert_eq!(
after.to_string(),
before,
"restarting after a kill must reuse the stored secret. A new NodeID \
here means every paired device sees a stranger and the user has to \
re-scan the pairing code."
);
}
const PEER_ID: &str =
"did:ad:node:1111111111111111111111111111111111111111111111111111111111111111";
const RELAY: &str = "https://relay.example/";
#[test]
#[ignore = "child process entry point, driven by the parent test"]
fn child_records_a_peer_then_dies() {
let Some(dir) = child_dir() else { return };
let runtime = tokio::runtime::Runtime::new().unwrap();
runtime.block_on(async {
let store = open_store(&dir).await;
atomic_lib::sync::peer::add_known_peer(&store, PEER_ID, "Paired phone");
atomic_lib::sync::peer::remember_peer_addr(
&store,
PEER_ID,
Some(RELAY.to_string()),
vec!["192.168.0.10:41234".to_string()],
);
std::mem::forget(store);
});
std::process::abort();
}
#[tokio::test]
async fn a_paired_peer_and_its_route_survive_an_unclean_kill() {
let dir = scratch_dir("peers");
run_child("child_records_a_peer_then_dies", &dir);
let store = open_store(&dir).await;
let peers = atomic_lib::sync::peer::get_known_peers(&store);
let peer = peers
.iter()
.find(|p| {
atomic_lib::sync::peer::normalize_node_id(&p.node_id)
== atomic_lib::sync::peer::normalize_node_id(PEER_ID)
})
.expect("the paired peer must still be known after a kill");
assert_eq!(peer.name, "Paired phone");
assert_eq!(
peer.relay_url.as_deref(),
Some(RELAY),
"without the stored relay, re-dialing falls back to a bare NodeID, \
which times out behind NAT"
);
assert_eq!(peer.direct_addrs, vec!["192.168.0.10:41234".to_string()]);
}