test: add unit tests for ConversationStore CRUD, outbox, and ConversationId

This commit is contained in:
2026-03-04 13:31:25 +01:00
parent e3dfc43e2c
commit e5329ee8e5
2 changed files with 183 additions and 0 deletions

View File

@@ -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());
}
}