use std::time::Duration;
use automerge::{ROOT, ReadDoc, transaction::Transactable};
use samod_core::{BackoffConfig, DialerConfig, DocSearchPhase, DocumentId};
use samod_test_harness::{Network, RunningDocIds};
fn init_logging() {
let _ = tracing_subscriber::fmt()
.with_env_filter(tracing_subscriber::EnvFilter::from_default_env())
.try_init();
}
fn non_retrying_dialer(url: &str) -> DialerConfig {
DialerConfig {
url: url::Url::parse(url).unwrap(),
backoff: BackoffConfig {
initial_delay: Duration::from_secs(999),
max_delay: Duration::from_secs(999),
max_retries: Some(0),
},
}
}
macro_rules! create_doc_with_data {
($network:expr, $peer:expr) => {{
let RunningDocIds { doc_id, actor_id } = $network.samod(&$peer).create_document();
$network
.samod(&$peer)
.with_document_by_actor(actor_id, |doc| {
let mut tx = doc.transaction();
tx.put(ROOT, "key", "value").unwrap();
tx.commit();
})
.unwrap();
doc_id
}};
}
#[test]
fn find_without_dialers_resolves_to_not_found() {
init_logging();
let mut network = Network::new();
let bob = network.create_samod("Bob");
let fake_doc_id = DocumentId::new(&mut rand::rng());
let result = network.samod(&bob).find_document(&fake_doc_id);
assert!(result.is_none(), "document should not be found");
}
#[test]
fn find_with_connecting_dialer_stays_pending() {
init_logging();
let mut network = Network::new();
let bob = network.create_samod("Bob");
let _dialer_id = network
.samod(&bob)
.add_dialer(non_retrying_dialer("wss://sync.example.com"));
let fake_doc_id = DocumentId::new(&mut rand::rng());
network.samod(&bob).search_for_doc(&fake_doc_id);
network.run_until_quiescent();
let search_status = network
.samod(&bob)
.search_status(&fake_doc_id)
.expect("search status should exist")
.clone();
assert!(
!search_status.is_currently_unavailable(),
"search should still be running while dialer is connecting"
);
}
#[test]
fn connected_with_long_handshake_does_not_mark_notfound() {
init_logging();
let mut network = Network::new();
let bob = network.create_samod("Bob");
let dialer_id = network
.samod(&bob)
.add_dialer(non_retrying_dialer("wss://sync.example.com"));
let fake_doc_id = DocumentId::new(&mut rand::rng());
network.samod(&bob).search_for_doc(&fake_doc_id);
network.run_until_quiescent();
network.samod(&bob).create_dialer_connection(dialer_id);
let search_status = network
.samod(&bob)
.search_status(&fake_doc_id)
.expect("search status should exist")
.clone();
assert!(
!search_status.is_currently_unavailable(),
"search should still be pending while dialer is connecting, got {search_status:?}"
);
}
#[test]
fn find_resolves_when_dialer_connects() {
init_logging();
let mut network = Network::new();
let alice = network.create_samod("Alice");
let bob = network.create_samod("Bob");
let doc_id = create_doc_with_data!(network, alice);
let dialer_id = network
.samod(&bob)
.add_dialer(non_retrying_dialer("wss://alice.example.com"));
network.connect_with_dialer(bob, dialer_id, alice);
network.run_until_quiescent();
network.samod(&bob).search_for_doc(&doc_id);
let search_status = network
.samod(&bob)
.search_status(&doc_id)
.expect("search status should exist")
.clone();
assert_eq!(search_status.phase(), &DocSearchPhase::Ready);
let bob_ref = network.samod(&bob);
let bob_doc = bob_ref.document(&doc_id).unwrap();
let val = bob_doc
.get(ROOT, "key")
.unwrap()
.map(|(v, _)| v.to_string())
.unwrap_or_default();
assert_eq!(val, r#""value""#);
}
#[test]
fn find_resolves_to_not_found_when_dialer_fails() {
init_logging();
let mut network = Network::new();
let bob = network.create_samod("Bob");
let dialer_id = network
.samod(&bob)
.add_dialer(non_retrying_dialer("wss://unreachable.example.com"));
let fake_doc_id = DocumentId::new(&mut rand::rng());
network.samod(&bob).search_for_doc(&fake_doc_id);
network.run_until_quiescent();
let search_status = network
.samod(&bob)
.search_status(&fake_doc_id)
.expect("search status should exist")
.clone();
assert!(matches!(
search_status.phase(),
DocSearchPhase::Searching(_)
));
assert!(
!search_status.is_currently_unavailable(),
"search should still be running while dialer is connecting"
);
network
.samod(&bob)
.dial_failed(dialer_id, "connection refused".to_string());
network.run_until_quiescent();
let search_status = network
.samod(&bob)
.search_status(&fake_doc_id)
.expect("search status should exist")
.clone();
assert!(
search_status.is_currently_unavailable(),
"search should resolve to not found after dialer fails: got {search_status:?}",
);
}
#[test]
fn waiting_to_retry_does_not_block_not_found() {
init_logging();
let mut network = Network::new();
let bob = network.create_samod("Bob");
let dialer_id = network.samod(&bob).add_dialer(DialerConfig {
url: url::Url::parse("wss://flaky.example.com").unwrap(),
backoff: BackoffConfig {
initial_delay: Duration::from_secs(999),
max_delay: Duration::from_secs(999),
max_retries: None, },
});
let fake_doc_id = DocumentId::new(&mut rand::rng());
network.samod(&bob).search_for_doc(&fake_doc_id);
network.run_until_quiescent();
let search_status = network
.samod(&bob)
.search_status(&fake_doc_id)
.unwrap()
.clone();
assert!(matches!(
search_status.phase(),
&DocSearchPhase::Searching(_)
));
assert!(
!search_status.is_currently_unavailable(),
"search should be pending while dialer is connecting"
);
network
.samod(&bob)
.dial_failed(dialer_id, "connection refused".to_string());
network.run_until_quiescent();
let search_status = network
.samod(&bob)
.search_status(&fake_doc_id)
.unwrap()
.clone();
assert!(
search_status.is_currently_unavailable(),
"search should resolve to unavailable when dialer is in WaitingToRetry: got {search_status:?}"
);
}
#[test]
fn removing_dialer_while_waiting_triggers_not_found() {
init_logging();
let mut network = Network::new();
let bob = network.create_samod("Bob");
let dialer_id = network
.samod(&bob)
.add_dialer(non_retrying_dialer("wss://sync.example.com"));
let fake_doc_id = DocumentId::new(&mut rand::rng());
network.samod(&bob).search_for_doc(&fake_doc_id);
network.run_until_quiescent();
let search_status = network
.samod(&bob)
.search_status(&fake_doc_id)
.unwrap()
.clone();
assert!(
!search_status.is_currently_unavailable(),
"find should be pending while dialer exists"
);
network.samod(&bob).remove_dialer(dialer_id);
network.run_until_quiescent();
let search_status = network
.samod(&bob)
.search_status(&fake_doc_id)
.unwrap()
.clone();
assert!(
search_status.is_currently_unavailable(),
"search should resolve to not found after dialer is removed: got {search_status:?}"
);
}
#[test]
fn multiple_dialers_stays_pending_while_any_connecting() {
init_logging();
let mut network = Network::new();
let bob = network.create_samod("Bob");
let dialer1 = network
.samod(&bob)
.add_dialer(non_retrying_dialer("wss://server1.example.com"));
let _dialer2 = network
.samod(&bob)
.add_dialer(non_retrying_dialer("wss://server2.example.com"));
let fake_doc_id = DocumentId::new(&mut rand::rng());
network.samod(&bob).search_for_doc(&fake_doc_id);
network.run_until_quiescent();
let search_status = network
.samod(&bob)
.search_status(&fake_doc_id)
.unwrap()
.clone();
assert!(matches!(
search_status.phase(),
DocSearchPhase::Searching(_)
));
assert!(
!search_status.is_currently_unavailable(),
"search should be pending with two connecting dialers"
);
network
.samod(&bob)
.dial_failed(dialer1, "connection refused".to_string());
network.run_until_quiescent();
let search_status = network
.samod(&bob)
.search_status(&fake_doc_id)
.unwrap()
.clone();
assert!(
!search_status.is_currently_unavailable(),
"search should still be pending while second dialer is connecting: got {search_status:?}"
);
}
#[test]
fn multiple_dialers_not_found_when_all_fail() {
init_logging();
let mut network = Network::new();
let bob = network.create_samod("Bob");
let dialer1 = network
.samod(&bob)
.add_dialer(non_retrying_dialer("wss://server1.example.com"));
let dialer2 = network
.samod(&bob)
.add_dialer(non_retrying_dialer("wss://server2.example.com"));
let fake_doc_id = DocumentId::new(&mut rand::rng());
network.samod(&bob).search_for_doc(&fake_doc_id);
network.run_until_quiescent();
network
.samod(&bob)
.dial_failed(dialer1, "connection refused".to_string());
network
.samod(&bob)
.dial_failed(dialer2, "connection refused".to_string());
network.run_until_quiescent();
let search_status = network
.samod(&bob)
.search_status(&fake_doc_id)
.unwrap()
.clone();
assert!(
search_status.is_currently_unavailable(),
"search should resolve to not found when all dialers have failed: got {search_status:?}"
);
}