test: add unit tests for ConversationStore CRUD, outbox, and ConversationId
This commit is contained in:
@@ -31,6 +31,7 @@ prost = { workspace = true }
|
||||
|
||||
[dev-dependencies]
|
||||
tokio = { workspace = true, features = ["test-util"] }
|
||||
tempfile = "3"
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
@@ -559,3 +559,185 @@ fn row_to_message(
|
||||
is_outgoing: is_outgoing != 0,
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
fn open_test_store() -> (tempfile::TempDir, ConversationStore) {
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
let db_path = dir.path().join("test.db");
|
||||
let store = ConversationStore::open(&db_path, None).unwrap();
|
||||
(dir, store)
|
||||
}
|
||||
|
||||
fn make_group_conv(name: &str, activity_ms: u64) -> Conversation {
|
||||
Conversation {
|
||||
id: ConversationId::from_group_name(name),
|
||||
kind: ConversationKind::Group { name: name.to_string() },
|
||||
display_name: format!("#{name}"),
|
||||
mls_group_blob: None,
|
||||
keystore_blob: None,
|
||||
member_keys: vec![vec![1, 2, 3]],
|
||||
unread_count: 0,
|
||||
last_activity_ms: activity_ms,
|
||||
created_at_ms: 1000,
|
||||
is_hybrid: false,
|
||||
last_seen_seq: 0,
|
||||
}
|
||||
}
|
||||
|
||||
fn make_dm_conv(peer_key: &[u8], peer_name: Option<&str>) -> Conversation {
|
||||
let mut id_bytes = [0u8; 16];
|
||||
id_bytes[..peer_key.len().min(16)].copy_from_slice(&peer_key[..peer_key.len().min(16)]);
|
||||
Conversation {
|
||||
id: ConversationId(id_bytes),
|
||||
kind: ConversationKind::Dm {
|
||||
peer_key: peer_key.to_vec(),
|
||||
peer_username: peer_name.map(|s| s.to_string()),
|
||||
},
|
||||
display_name: peer_name.unwrap_or("unknown").to_string(),
|
||||
mls_group_blob: None,
|
||||
keystore_blob: None,
|
||||
member_keys: vec![peer_key.to_vec()],
|
||||
unread_count: 0,
|
||||
last_activity_ms: 2000,
|
||||
created_at_ms: 1000,
|
||||
is_hybrid: false,
|
||||
last_seen_seq: 0,
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn save_and_load_group() {
|
||||
let (_dir, store) = open_test_store();
|
||||
let conv = make_group_conv("engineering", 5000);
|
||||
store.save_conversation(&conv).unwrap();
|
||||
|
||||
let loaded = store.load_conversation(&conv.id).unwrap().unwrap();
|
||||
assert_eq!(loaded.display_name, "#engineering");
|
||||
assert_eq!(loaded.last_activity_ms, 5000);
|
||||
match &loaded.kind {
|
||||
ConversationKind::Group { name } => assert_eq!(name, "engineering"),
|
||||
_ => panic!("expected Group kind"),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn save_and_load_dm() {
|
||||
let (_dir, store) = open_test_store();
|
||||
let peer_key = vec![10u8; 32];
|
||||
let conv = make_dm_conv(&peer_key, Some("alice"));
|
||||
store.save_conversation(&conv).unwrap();
|
||||
|
||||
let loaded = store.load_conversation(&conv.id).unwrap().unwrap();
|
||||
assert_eq!(loaded.display_name, "alice");
|
||||
match &loaded.kind {
|
||||
ConversationKind::Dm { peer_key: pk, peer_username } => {
|
||||
assert_eq!(pk, &peer_key);
|
||||
assert_eq!(peer_username.as_deref(), Some("alice"));
|
||||
}
|
||||
_ => panic!("expected Dm kind"),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn find_dm_by_peer() {
|
||||
let (_dir, store) = open_test_store();
|
||||
let peer_key = vec![20u8; 32];
|
||||
let conv = make_dm_conv(&peer_key, Some("bob"));
|
||||
store.save_conversation(&conv).unwrap();
|
||||
|
||||
let found = store.find_dm_by_peer(&peer_key).unwrap().unwrap();
|
||||
assert_eq!(found.id, conv.id);
|
||||
|
||||
let missing = store.find_dm_by_peer(&[99u8; 32]).unwrap();
|
||||
assert!(missing.is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn list_conversations_ordering() {
|
||||
let (_dir, store) = open_test_store();
|
||||
let c1 = make_group_conv("old-group", 1000);
|
||||
let c2 = make_group_conv("new-group", 3000);
|
||||
let c3 = make_group_conv("mid-group", 2000);
|
||||
store.save_conversation(&c1).unwrap();
|
||||
store.save_conversation(&c2).unwrap();
|
||||
store.save_conversation(&c3).unwrap();
|
||||
|
||||
let list = store.list_conversations().unwrap();
|
||||
assert_eq!(list.len(), 3);
|
||||
// Most recent activity first
|
||||
assert_eq!(list[0].last_activity_ms, 3000);
|
||||
assert_eq!(list[1].last_activity_ms, 2000);
|
||||
assert_eq!(list[2].last_activity_ms, 1000);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn save_and_load_messages() {
|
||||
let (_dir, store) = open_test_store();
|
||||
let conv = make_group_conv("chat", 1000);
|
||||
store.save_conversation(&conv).unwrap();
|
||||
|
||||
for i in 0..5 {
|
||||
store.save_message(&StoredMessage {
|
||||
conversation_id: conv.id.clone(),
|
||||
message_id: None,
|
||||
sender_key: vec![1, 2, 3],
|
||||
sender_name: Some("alice".to_string()),
|
||||
body: format!("message {i}"),
|
||||
msg_type: "chat".to_string(),
|
||||
ref_msg_id: None,
|
||||
timestamp_ms: 1000 + i as u64,
|
||||
is_outgoing: i % 2 == 0,
|
||||
}).unwrap();
|
||||
}
|
||||
|
||||
let msgs = store.load_recent_messages(&conv.id, 3).unwrap();
|
||||
assert_eq!(msgs.len(), 3);
|
||||
// Should be in chronological order (reversed from DESC)
|
||||
assert_eq!(msgs[0].body, "message 2");
|
||||
assert_eq!(msgs[1].body, "message 3");
|
||||
assert_eq!(msgs[2].body, "message 4");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn outbox_enqueue_and_mark_sent() {
|
||||
let (_dir, store) = open_test_store();
|
||||
let conv_id = ConversationId([1u8; 16]);
|
||||
let recipient = vec![5u8; 32];
|
||||
let payload = b"encrypted-payload";
|
||||
|
||||
store.enqueue_outbox(&conv_id, &recipient, payload).unwrap();
|
||||
store.enqueue_outbox(&conv_id, &recipient, b"second").unwrap();
|
||||
|
||||
let pending = store.load_pending_outbox().unwrap();
|
||||
assert_eq!(pending.len(), 2);
|
||||
assert_eq!(store.count_pending_outbox().unwrap(), 2);
|
||||
|
||||
store.mark_outbox_sent(pending[0].id).unwrap();
|
||||
assert_eq!(store.count_pending_outbox().unwrap(), 1);
|
||||
|
||||
let remaining = store.load_pending_outbox().unwrap();
|
||||
assert_eq!(remaining.len(), 1);
|
||||
assert_eq!(remaining[0].payload, b"second");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn conversation_id_from_group_name_determinism() {
|
||||
let a = ConversationId::from_group_name("test-group");
|
||||
let b = ConversationId::from_group_name("test-group");
|
||||
assert_eq!(a, b);
|
||||
|
||||
let c = ConversationId::from_group_name("other-group");
|
||||
assert_ne!(a, c);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn conversation_id_from_slice_wrong_length() {
|
||||
assert!(ConversationId::from_slice(&[0u8; 15]).is_none());
|
||||
assert!(ConversationId::from_slice(&[0u8; 17]).is_none());
|
||||
assert!(ConversationId::from_slice(&[]).is_none());
|
||||
assert!(ConversationId::from_slice(&[0u8; 16]).is_some());
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user