use communitas_core::crdt::*;
use communitas_core::message_sync::MessageSyncService;
use proptest::prelude::*;
fn arb_peer_id() -> impl Strategy<Value = String> {
"[a-z]{4}-[a-z]{4}-[a-z]{4}-[a-z]{4}".prop_map(|s| s.to_string())
}
fn arb_entity_id() -> impl Strategy<Value = String> {
prop::sample::select(vec![
"contact-alice".to_string(),
"project-communitas".to_string(),
"saorsa-general".to_string(),
"group-test".to_string(),
"saorsa-org".to_string(),
])
}
proptest! {
#[test]
fn prop_send_increments_clock(
peer_id in arb_peer_id(),
entity_id in arb_entity_id()
) {
tokio_test::block_on(async {
let service = MessageSyncService::new(peer_id.clone());
let msg1 = service
.send_message(
entity_id.clone(),
EntityType::Person,
MessageContent {
text: "First message".to_string(),
author: "Test".to_string(),
attachments: None,
},
None,
)
.await
.map_err(|e| TestCaseError::fail(e.to_string()))?;
prop_assert_eq!(msg1.metadata.vector_clock.0.get(&peer_id), Some(&1));
let msg2 = service
.send_message(
entity_id.clone(),
EntityType::Person,
MessageContent {
text: "Second message".to_string(),
author: "Test".to_string(),
attachments: None,
},
None,
)
.await
.map_err(|e| TestCaseError::fail(e.to_string()))?;
prop_assert_eq!(msg2.metadata.vector_clock.0.get(&peer_id), Some(&2));
Ok(())
})?;
}
#[test]
fn prop_lamport_always_increases(
peer_id in arb_peer_id(),
entity_id in arb_entity_id(),
count in 2usize..10
) {
tokio_test::block_on(async {
let service = MessageSyncService::new(peer_id.clone());
let mut last_lamport = 0;
for i in 0..count {
let msg = service
.send_message(
entity_id.clone(),
EntityType::Person,
MessageContent {
text: format!("Message {}", i),
author: "Test".to_string(),
attachments: None,
},
None,
)
.await
.map_err(|e| TestCaseError::fail(e.to_string()))?;
prop_assert!(msg.metadata.lamport_clock > last_lamport);
last_lamport = msg.metadata.lamport_clock;
}
Ok(())
})?;
}
#[test]
fn prop_in_order_messages_accepted(
peer_id1 in arb_peer_id(),
peer_id2 in arb_peer_id(),
entity_id in arb_entity_id()
) {
tokio_test::block_on(async {
if peer_id1 == peer_id2 {
return Ok(());
}
let sender = MessageSyncService::new(peer_id1.clone());
let receiver = MessageSyncService::new(peer_id2.clone());
for i in 0..5 {
let msg = sender
.send_message(
entity_id.clone(),
EntityType::Person,
MessageContent {
text: format!("Message {}", i),
author: "Sender".to_string(),
attachments: None,
},
None,
)
.await
.map_err(|e| TestCaseError::fail(e.to_string()))?;
let result = receiver
.receive_message(msg)
.await
.map_err(|e| TestCaseError::fail(e.to_string()))?;
prop_assert!(result.accepted);
prop_assert!(!result.out_of_order);
}
Ok(())
})?;
}
#[test]
fn prop_get_all_messages_causally_ordered(
peer_id in arb_peer_id(),
entity_id in arb_entity_id(),
count in 2usize..20
) {
tokio_test::block_on(async {
let service = MessageSyncService::new(peer_id.clone());
for i in 0..count {
service
.send_message(
entity_id.clone(),
EntityType::Person,
MessageContent {
text: format!("Message {}", i),
author: "Test".to_string(),
attachments: None,
},
None,
)
.await
.map_err(|e| TestCaseError::fail(e.to_string()))?;
}
let response = service
.get_all_messages(&entity_id)
.await
.map_err(|e| TestCaseError::fail(e.to_string()))?;
for i in 0..response.messages.len().saturating_sub(1) {
let ord = response.messages[i]
.metadata
.vector_clock
.compare(&response.messages[i + 1].metadata.vector_clock);
prop_assert!(!matches!(ord, ClockOrdering::After));
}
Ok(())
})?;
}
}
#[tokio::test]
async fn test_two_peers_bidirectional_sync() {
let peer1 = MessageSyncService::new("peer-one-two-three-four".to_string());
let peer2 = MessageSyncService::new("peer-five-six-seven-eight".to_string());
let entity_id = "contact-alice";
for i in 0..3 {
peer1
.send_message(
entity_id.to_string(),
EntityType::Person,
MessageContent {
text: format!("Peer1 message {}", i),
author: "Peer1".to_string(),
attachments: None,
},
None,
)
.await
.unwrap();
}
for i in 0..2 {
peer2
.send_message(
entity_id.to_string(),
EntityType::Person,
MessageContent {
text: format!("Peer2 message {}", i),
author: "Peer2".to_string(),
attachments: None,
},
None,
)
.await
.unwrap();
}
let peer2_messages = peer2.get_all_messages(entity_id).await.unwrap();
assert_eq!(peer2_messages.messages.len(), 2); let result1 = peer1.handle_sync_response(peer2_messages).await.unwrap();
assert_eq!(result1.messages_added, 2);
let peer1_messages = peer1.get_all_messages(entity_id).await.unwrap();
assert_eq!(peer1_messages.messages.len(), 5); let result2 = peer2.handle_sync_response(peer1_messages).await.unwrap();
assert_eq!(result2.messages_added, 5);
assert_eq!(result2.messages_rejected, 0);
let peer1_final = peer1.get_messages(entity_id).await.unwrap();
let peer2_final = peer2.get_messages(entity_id).await.unwrap();
assert_eq!(peer1_final.len(), 5);
assert_eq!(peer2_final.len(), 5);
for i in 0..5 {
assert_eq!(peer1_final[i].metadata.id, peer2_final[i].metadata.id);
}
}
#[tokio::test]
async fn test_out_of_order_message_queued() {
let sender = MessageSyncService::new("peer-sender".to_string());
let receiver = MessageSyncService::new("peer-receiver".to_string());
let entity_id = "contact-test";
let msg1 = sender
.send_message(
entity_id.to_string(),
EntityType::Person,
MessageContent {
text: "Message 1".to_string(),
author: "Sender".to_string(),
attachments: None,
},
None,
)
.await
.unwrap();
let msg2 = sender
.send_message(
entity_id.to_string(),
EntityType::Person,
MessageContent {
text: "Message 2".to_string(),
author: "Sender".to_string(),
attachments: None,
},
None,
)
.await
.unwrap();
let msg3 = sender
.send_message(
entity_id.to_string(),
EntityType::Person,
MessageContent {
text: "Message 3".to_string(),
author: "Sender".to_string(),
attachments: None,
},
None,
)
.await
.unwrap();
let result1 = receiver.receive_message(msg1).await.unwrap();
assert!(result1.accepted);
let result3 = receiver.receive_message(msg3.clone()).await.unwrap();
assert!(!result3.accepted);
assert!(result3.out_of_order);
assert!(result3.missing_ranges.is_some());
let state = receiver.get_sync_state(entity_id).await.unwrap();
assert_eq!(state.out_of_order_messages.len(), 1);
let result2 = receiver.receive_message(msg2).await.unwrap();
assert!(result2.accepted);
let messages = receiver.get_messages(entity_id).await.unwrap();
assert_eq!(messages.len(), 3);
assert_eq!(messages.len(), 3);
for msg in &messages {
assert!(msg.metadata.author_peer_id == "peer-sender");
}
}
#[tokio::test]
async fn test_missing_message_ranges() {
let sender = MessageSyncService::new("peer-sender".to_string());
let receiver = MessageSyncService::new("peer-receiver".to_string());
let entity_id = "contact-test";
for i in 0..10 {
sender
.send_message(
entity_id.to_string(),
EntityType::Person,
MessageContent {
text: format!("Message {}", i),
author: "Sender".to_string(),
attachments: None,
},
None,
)
.await
.unwrap();
}
let all_messages = sender.get_messages(entity_id).await.unwrap();
receiver
.receive_message(all_messages[0].clone())
.await
.unwrap();
receiver
.receive_message(all_messages[1].clone())
.await
.unwrap();
receiver
.receive_message(all_messages[2].clone())
.await
.unwrap();
let result = receiver
.receive_message(all_messages[9].clone())
.await
.unwrap();
assert!(!result.accepted);
assert!(result.out_of_order);
let missing = result.missing_ranges.unwrap();
assert!(!missing.is_empty());
let peer_sender_range = missing.iter().find(|r| r.peer_id == "peer-sender").unwrap();
assert_eq!(peer_sender_range.from_timestamp, 4);
assert_eq!(peer_sender_range.to_timestamp, 10);
}
#[tokio::test]
async fn test_three_peer_convergence() {
let peer1 = MessageSyncService::new("peer-one".to_string());
let peer2 = MessageSyncService::new("peer-two".to_string());
let peer3 = MessageSyncService::new("peer-three".to_string());
let entity_id = "project-communitas";
for i in 0..2 {
peer1
.send_message(
entity_id.to_string(),
EntityType::Project,
MessageContent {
text: format!("Peer1 msg {}", i),
author: "Peer1".to_string(),
attachments: None,
},
None,
)
.await
.unwrap();
peer2
.send_message(
entity_id.to_string(),
EntityType::Project,
MessageContent {
text: format!("Peer2 msg {}", i),
author: "Peer2".to_string(),
attachments: None,
},
None,
)
.await
.unwrap();
peer3
.send_message(
entity_id.to_string(),
EntityType::Project,
MessageContent {
text: format!("Peer3 msg {}", i),
author: "Peer3".to_string(),
attachments: None,
},
None,
)
.await
.unwrap();
}
let peer2_sync = peer2.get_all_messages(entity_id).await.unwrap();
peer1.handle_sync_response(peer2_sync).await.unwrap();
let peer1_sync = peer1.get_all_messages(entity_id).await.unwrap();
peer2.handle_sync_response(peer1_sync).await.unwrap();
let peer3_sync = peer3.get_all_messages(entity_id).await.unwrap();
peer2.handle_sync_response(peer3_sync).await.unwrap();
let peer2_sync2 = peer2.get_all_messages(entity_id).await.unwrap();
peer3.handle_sync_response(peer2_sync2).await.unwrap();
let peer3_sync2 = peer3.get_all_messages(entity_id).await.unwrap();
peer1.handle_sync_response(peer3_sync2).await.unwrap();
let peer1_sync2 = peer1.get_all_messages(entity_id).await.unwrap();
peer3.handle_sync_response(peer1_sync2).await.unwrap();
let peer1_messages = peer1.get_messages(entity_id).await.unwrap();
let peer2_messages = peer2.get_messages(entity_id).await.unwrap();
let peer3_messages = peer3.get_messages(entity_id).await.unwrap();
assert_eq!(peer1_messages.len(), 6);
assert_eq!(peer2_messages.len(), 6);
assert_eq!(peer3_messages.len(), 6);
for i in 0..6 {
assert_eq!(peer1_messages[i].metadata.id, peer2_messages[i].metadata.id);
assert_eq!(peer2_messages[i].metadata.id, peer3_messages[i].metadata.id);
}
}
#[tokio::test]
async fn test_sync_state_tracking() {
let service = MessageSyncService::new("peer-test".to_string());
let entity_id = "contact-alice";
let state0 = service.get_sync_state(entity_id).await.unwrap();
assert_eq!(state0.message_count, 0);
assert!(state0.missing_messages.is_empty());
for i in 0..3 {
service
.send_message(
entity_id.to_string(),
EntityType::Person,
MessageContent {
text: format!("Message {}", i),
author: "Test".to_string(),
attachments: None,
},
None,
)
.await
.unwrap();
}
let state1 = service.get_sync_state(entity_id).await.unwrap();
assert_eq!(state1.message_count, 3);
assert_eq!(state1.vector_clock.0.get("peer-test"), Some(&3));
}
#[tokio::test]
async fn test_needs_sync_detection() {
let peer1 = MessageSyncService::new("peer-one".to_string());
let peer2 = MessageSyncService::new("peer-two".to_string());
let entity_id = "contact-test";
let peer1_clock = VectorClock::new();
assert!(!peer2.needs_sync(entity_id, &peer1_clock).await);
peer1
.send_message(
entity_id.to_string(),
EntityType::Person,
MessageContent {
text: "Hello".to_string(),
author: "Peer1".to_string(),
attachments: None,
},
None,
)
.await
.unwrap();
let state = peer1.get_sync_state(entity_id).await.unwrap();
let peer1_clock = state.vector_clock;
assert!(peer2.needs_sync(entity_id, &peer1_clock).await);
let sync_response = peer1.get_all_messages(entity_id).await.unwrap();
peer2.handle_sync_response(sync_response).await.unwrap();
assert!(!peer2.needs_sync(entity_id, &peer1_clock).await);
}
#[tokio::test]
async fn test_duplicate_message_ignored() {
let service = MessageSyncService::new("peer-test".to_string());
let entity_id = "contact-alice";
let msg = service
.send_message(
entity_id.to_string(),
EntityType::Person,
MessageContent {
text: "Message".to_string(),
author: "Test".to_string(),
attachments: None,
},
None,
)
.await
.unwrap();
let _result = service.receive_message(msg.clone()).await.unwrap();
let messages = service.get_messages(entity_id).await.unwrap();
assert_eq!(messages.len(), 1); }