test: add unit tests for ConversationStore CRUD, outbox, and ConversationId
This commit is contained in:
@@ -31,6 +31,7 @@ prost = { workspace = true }
|
|||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
tokio = { workspace = true, features = ["test-util"] }
|
tokio = { workspace = true, features = ["test-util"] }
|
||||||
|
tempfile = "3"
|
||||||
|
|
||||||
[lints]
|
[lints]
|
||||||
workspace = true
|
workspace = true
|
||||||
|
|||||||
@@ -559,3 +559,185 @@ fn row_to_message(
|
|||||||
is_outgoing: is_outgoing != 0,
|
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