test: add unit tests for RPC framing, SDK state machine, and server domain services
Add comprehensive tests across three layers: - RPC framing: empty payloads, max boundary, truncated frames, multi-frame buffers, all status codes, all method ID ranges, payload-too-large for response/push - SDK: event broadcast send/receive, multiple subscribers, clone preservation, conversation upsert, missing conversation, message ID roundtrip, member keys - Server domain: auth session validation/expiry, channel creation/symmetry/validation, delivery peek/ack/sequence ordering/fetch-limited, key package upload/fetch/validation, hybrid key batch fetch, size boundary tests - CI: MSRV (1.75) check job, macOS cross-platform build check
This commit is contained in:
@@ -1038,4 +1038,83 @@ mod tests {
|
||||
// Returns 0 for unknown conversations.
|
||||
assert_eq!(store.get_last_seen_seq(&missing).unwrap(), 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn upsert_conversation_updates_fields() {
|
||||
let (_dir, store) = open_test_store();
|
||||
let mut conv = make_group_conv("updatable", 1000);
|
||||
store.save_conversation(&conv).unwrap();
|
||||
|
||||
// Update display name and activity
|
||||
conv.display_name = "#updated".to_string();
|
||||
conv.last_activity_ms = 9000;
|
||||
conv.unread_count = 5;
|
||||
conv.is_hybrid = true;
|
||||
store.save_conversation(&conv).unwrap();
|
||||
|
||||
let loaded = store.load_conversation(&conv.id).unwrap().unwrap();
|
||||
assert_eq!(loaded.display_name, "#updated");
|
||||
assert_eq!(loaded.last_activity_ms, 9000);
|
||||
assert_eq!(loaded.unread_count, 5);
|
||||
assert!(loaded.is_hybrid);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn load_missing_conversation_returns_none() {
|
||||
let (_dir, store) = open_test_store();
|
||||
let missing = ConversationId([0xFFu8; 16]);
|
||||
assert!(store.load_conversation(&missing).unwrap().is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn conversation_id_hex_encoding() {
|
||||
let id = ConversationId([0xAB; 16]);
|
||||
assert_eq!(id.hex(), "abababababababababababababababab");
|
||||
assert_eq!(id.hex().len(), 32);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn save_message_with_message_id_and_ref() {
|
||||
let (_dir, store) = open_test_store();
|
||||
let conv = make_group_conv("msg-ids", 1000);
|
||||
store.save_conversation(&conv).unwrap();
|
||||
|
||||
let msg_id = [42u8; 16];
|
||||
let ref_id = [99u8; 16];
|
||||
store.save_message(&StoredMessage {
|
||||
conversation_id: conv.id.clone(),
|
||||
message_id: Some(msg_id),
|
||||
sender_key: vec![1, 2, 3],
|
||||
sender_name: Some("alice".to_string()),
|
||||
body: "reply".to_string(),
|
||||
msg_type: "chat".to_string(),
|
||||
ref_msg_id: Some(ref_id),
|
||||
timestamp_ms: 5000,
|
||||
is_outgoing: true,
|
||||
}).unwrap();
|
||||
|
||||
let msgs = store.load_recent_messages(&conv.id, 10).unwrap();
|
||||
assert_eq!(msgs.len(), 1);
|
||||
assert_eq!(msgs[0].message_id, Some(msg_id));
|
||||
assert_eq!(msgs[0].ref_msg_id, Some(ref_id));
|
||||
assert!(msgs[0].is_outgoing);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn member_keys_serialization_roundtrip() {
|
||||
let (_dir, store) = open_test_store();
|
||||
let mut conv = make_group_conv("member-keys", 1000);
|
||||
conv.member_keys = vec![
|
||||
vec![1u8; 32],
|
||||
vec![2u8; 32],
|
||||
vec![3u8; 32],
|
||||
];
|
||||
store.save_conversation(&conv).unwrap();
|
||||
|
||||
let loaded = store.load_conversation(&conv.id).unwrap().unwrap();
|
||||
assert_eq!(loaded.member_keys.len(), 3);
|
||||
assert_eq!(loaded.member_keys[0], vec![1u8; 32]);
|
||||
assert_eq!(loaded.member_keys[1], vec![2u8; 32]);
|
||||
assert_eq!(loaded.member_keys[2], vec![3u8; 32]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -85,3 +85,145 @@ pub enum ClientEvent {
|
||||
/// An error occurred in the background.
|
||||
Error { message: String },
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use tokio::sync::broadcast;
|
||||
|
||||
#[test]
|
||||
fn event_broadcast_send_receive() {
|
||||
let (tx, mut rx) = broadcast::channel::<ClientEvent>(16);
|
||||
|
||||
tx.send(ClientEvent::Connected).unwrap();
|
||||
tx.send(ClientEvent::Disconnected {
|
||||
reason: "test".into(),
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
let e1 = rx.try_recv().unwrap();
|
||||
assert!(matches!(e1, ClientEvent::Connected));
|
||||
|
||||
let e2 = rx.try_recv().unwrap();
|
||||
assert!(matches!(e2, ClientEvent::Disconnected { reason } if reason == "test"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn event_broadcast_multiple_subscribers() {
|
||||
let (tx, mut rx1) = broadcast::channel::<ClientEvent>(16);
|
||||
let mut rx2 = tx.subscribe();
|
||||
|
||||
tx.send(ClientEvent::Registered {
|
||||
username: "alice".into(),
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
let e1 = rx1.try_recv().unwrap();
|
||||
let e2 = rx2.try_recv().unwrap();
|
||||
|
||||
assert!(matches!(e1, ClientEvent::Registered { username } if username == "alice"));
|
||||
assert!(matches!(e2, ClientEvent::Registered { username } if username == "alice"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn event_no_subscribers_does_not_panic() {
|
||||
let (tx, _) = broadcast::channel::<ClientEvent>(16);
|
||||
// Send with no active receiver — should return Err but not panic.
|
||||
let result = tx.send(ClientEvent::Connected);
|
||||
assert!(result.is_err()); // no receivers
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn event_clone_preserves_data() {
|
||||
let event = ClientEvent::MessageReceived {
|
||||
conversation_id: [1u8; 16],
|
||||
sender_key: vec![2u8; 32],
|
||||
sender_name: Some("bob".into()),
|
||||
body: "hello world".into(),
|
||||
timestamp_ms: 12345,
|
||||
};
|
||||
|
||||
let cloned = event.clone();
|
||||
match cloned {
|
||||
ClientEvent::MessageReceived {
|
||||
conversation_id,
|
||||
sender_key,
|
||||
sender_name,
|
||||
body,
|
||||
timestamp_ms,
|
||||
} => {
|
||||
assert_eq!(conversation_id, [1u8; 16]);
|
||||
assert_eq!(sender_key, vec![2u8; 32]);
|
||||
assert_eq!(sender_name, Some("bob".to_string()));
|
||||
assert_eq!(body, "hello world");
|
||||
assert_eq!(timestamp_ms, 12345);
|
||||
}
|
||||
_ => panic!("wrong variant after clone"),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn event_debug_format() {
|
||||
let event = ClientEvent::Error {
|
||||
message: "something went wrong".into(),
|
||||
};
|
||||
let dbg = format!("{event:?}");
|
||||
assert!(dbg.contains("something went wrong"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn all_event_variants_are_clone() {
|
||||
// Verify all variants can be cloned without issue.
|
||||
let events: Vec<ClientEvent> = vec![
|
||||
ClientEvent::Connected,
|
||||
ClientEvent::Disconnected { reason: "r".into() },
|
||||
ClientEvent::Reconnecting { attempt: 1 },
|
||||
ClientEvent::Registered { username: "u".into() },
|
||||
ClientEvent::LoggedIn { username: "u".into() },
|
||||
ClientEvent::LoggedOut { username: "u".into() },
|
||||
ClientEvent::Authenticated { username: "u".into() },
|
||||
ClientEvent::MessageReceived {
|
||||
conversation_id: [0; 16],
|
||||
sender_key: vec![],
|
||||
sender_name: None,
|
||||
body: "b".into(),
|
||||
timestamp_ms: 0,
|
||||
},
|
||||
ClientEvent::MessageSent {
|
||||
conversation_id: [0; 16],
|
||||
seq: 0,
|
||||
},
|
||||
ClientEvent::ConversationCreated {
|
||||
conversation_id: [0; 16],
|
||||
display_name: "d".into(),
|
||||
},
|
||||
ClientEvent::MemberAdded {
|
||||
conversation_id: [0; 16],
|
||||
member_key: vec![],
|
||||
},
|
||||
ClientEvent::MemberRemoved {
|
||||
conversation_id: [0; 16],
|
||||
member_key: vec![],
|
||||
},
|
||||
ClientEvent::PushEvent {
|
||||
event_type: 0,
|
||||
payload: vec![],
|
||||
},
|
||||
ClientEvent::MessageQueued {
|
||||
outbox_id: 0,
|
||||
conversation_id: [0; 16],
|
||||
},
|
||||
ClientEvent::OutboxFlushed { sent: 0, failed: 0 },
|
||||
ClientEvent::MessageGap {
|
||||
conversation_id: [0; 16],
|
||||
expected_seq: 0,
|
||||
received_seq: 1,
|
||||
},
|
||||
ClientEvent::Error { message: "e".into() },
|
||||
];
|
||||
for event in &events {
|
||||
let _ = event.clone();
|
||||
}
|
||||
assert_eq!(events.len(), 17);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user