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:
@@ -277,4 +277,241 @@ mod tests {
|
||||
let decoded = RequestFrame::decode(&mut buf).expect("decode").expect("complete");
|
||||
assert!(decoded.payload.is_empty());
|
||||
}
|
||||
|
||||
// ── Additional framing tests ────────────────────────────────────────────
|
||||
|
||||
#[test]
|
||||
fn empty_payload_response() {
|
||||
let frame = ResponseFrame {
|
||||
status: RpcStatus::NotFound as u8,
|
||||
request_id: 999,
|
||||
payload: Bytes::new(),
|
||||
};
|
||||
let encoded = frame.encode();
|
||||
assert_eq!(encoded.len(), RESPONSE_HEADER_SIZE);
|
||||
let mut buf = BytesMut::from(encoded.as_ref());
|
||||
let decoded = ResponseFrame::decode(&mut buf).expect("decode").expect("complete");
|
||||
assert!(decoded.payload.is_empty());
|
||||
assert_eq!(decoded.status, RpcStatus::NotFound as u8);
|
||||
assert_eq!(decoded.request_id, 999);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn empty_payload_push() {
|
||||
let frame = PushFrame {
|
||||
event_type: 0,
|
||||
payload: Bytes::new(),
|
||||
};
|
||||
let encoded = frame.encode();
|
||||
assert_eq!(encoded.len(), PUSH_HEADER_SIZE);
|
||||
let mut buf = BytesMut::from(encoded.as_ref());
|
||||
let decoded = PushFrame::decode(&mut buf).expect("decode").expect("complete");
|
||||
assert!(decoded.payload.is_empty());
|
||||
assert_eq!(decoded.event_type, 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn max_boundary_payload_request() {
|
||||
// Exactly MAX_PAYLOAD_SIZE should succeed (not exceed limit).
|
||||
let payload = vec![0xABu8; MAX_PAYLOAD_SIZE];
|
||||
let frame = RequestFrame {
|
||||
method_id: 1,
|
||||
request_id: 1,
|
||||
payload: Bytes::from(payload.clone()),
|
||||
};
|
||||
let encoded = frame.encode();
|
||||
assert_eq!(encoded.len(), REQUEST_HEADER_SIZE + MAX_PAYLOAD_SIZE);
|
||||
let mut buf = BytesMut::from(encoded.as_ref());
|
||||
let decoded = RequestFrame::decode(&mut buf).expect("decode").expect("complete");
|
||||
assert_eq!(decoded.payload.len(), MAX_PAYLOAD_SIZE);
|
||||
assert_eq!(decoded.payload[0], 0xAB);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn response_payload_too_large_rejected() {
|
||||
let mut buf = BytesMut::new();
|
||||
buf.put_u8(0); // status OK
|
||||
buf.put_u32(1); // request_id
|
||||
buf.put_u32((MAX_PAYLOAD_SIZE + 1) as u32);
|
||||
let result = ResponseFrame::decode(&mut buf);
|
||||
assert!(matches!(result, Err(RpcError::PayloadTooLarge { .. })));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn push_payload_too_large_rejected() {
|
||||
let mut buf = BytesMut::new();
|
||||
buf.put_u16(1); // event_type
|
||||
buf.put_u32((MAX_PAYLOAD_SIZE + 1) as u32);
|
||||
let result = PushFrame::decode(&mut buf);
|
||||
assert!(matches!(result, Err(RpcError::PayloadTooLarge { .. })));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn incomplete_response_returns_none() {
|
||||
// Less than RESPONSE_HEADER_SIZE bytes
|
||||
let mut buf = BytesMut::from(&[0u8; 4][..]);
|
||||
assert!(ResponseFrame::decode(&mut buf).expect("no error").is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn incomplete_push_returns_none() {
|
||||
// Less than PUSH_HEADER_SIZE bytes
|
||||
let mut buf = BytesMut::from(&[0u8; 3][..]);
|
||||
assert!(PushFrame::decode(&mut buf).expect("no error").is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn request_header_present_but_payload_incomplete() {
|
||||
// Full header but payload not yet received
|
||||
let frame = RequestFrame {
|
||||
method_id: 10,
|
||||
request_id: 20,
|
||||
payload: Bytes::from_static(b"abcdefgh"),
|
||||
};
|
||||
let encoded = frame.encode();
|
||||
// Truncate to header + 3 bytes of payload (need 8)
|
||||
let mut buf = BytesMut::from(&encoded[..REQUEST_HEADER_SIZE + 3]);
|
||||
assert!(RequestFrame::decode(&mut buf).expect("no error").is_none());
|
||||
// Buffer should be untouched (not consumed)
|
||||
assert_eq!(buf.len(), REQUEST_HEADER_SIZE + 3);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn response_header_present_but_payload_incomplete() {
|
||||
let frame = ResponseFrame {
|
||||
status: 0,
|
||||
request_id: 1,
|
||||
payload: Bytes::from_static(b"abcdefgh"),
|
||||
};
|
||||
let encoded = frame.encode();
|
||||
let mut buf = BytesMut::from(&encoded[..RESPONSE_HEADER_SIZE + 2]);
|
||||
assert!(ResponseFrame::decode(&mut buf).expect("no error").is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn push_header_present_but_payload_incomplete() {
|
||||
let frame = PushFrame {
|
||||
event_type: 1,
|
||||
payload: Bytes::from_static(b"abcdefgh"),
|
||||
};
|
||||
let encoded = frame.encode();
|
||||
let mut buf = BytesMut::from(&encoded[..PUSH_HEADER_SIZE + 2]);
|
||||
assert!(PushFrame::decode(&mut buf).expect("no error").is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn request_zero_length_prefix() {
|
||||
// Zero-length payload in the header is valid (empty payload)
|
||||
let mut buf = BytesMut::new();
|
||||
buf.put_u16(5); // method_id
|
||||
buf.put_u32(10); // request_id
|
||||
buf.put_u32(0); // payload_len = 0
|
||||
let decoded = RequestFrame::decode(&mut buf).expect("decode").expect("complete");
|
||||
assert_eq!(decoded.method_id, 5);
|
||||
assert_eq!(decoded.request_id, 10);
|
||||
assert!(decoded.payload.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn response_rpc_status_conversion() {
|
||||
let frame = ResponseFrame {
|
||||
status: RpcStatus::Unauthorized as u8,
|
||||
request_id: 1,
|
||||
payload: Bytes::new(),
|
||||
};
|
||||
assert_eq!(frame.rpc_status(), Some(RpcStatus::Unauthorized));
|
||||
|
||||
let unknown = ResponseFrame {
|
||||
status: 255,
|
||||
request_id: 1,
|
||||
payload: Bytes::new(),
|
||||
};
|
||||
assert_eq!(unknown.rpc_status(), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn all_method_ids_roundtrip() {
|
||||
// Test a selection of method IDs spanning the full u16 range
|
||||
let method_ids: &[u16] = &[0, 1, 100, 200, 300, 400, 500, 1000, u16::MAX];
|
||||
for &mid in method_ids {
|
||||
let frame = RequestFrame {
|
||||
method_id: mid,
|
||||
request_id: mid as u32,
|
||||
payload: Bytes::from_static(b"x"),
|
||||
};
|
||||
let encoded = frame.encode();
|
||||
let mut buf = BytesMut::from(encoded.as_ref());
|
||||
let decoded = RequestFrame::decode(&mut buf).unwrap().unwrap();
|
||||
assert_eq!(decoded.method_id, mid);
|
||||
assert_eq!(decoded.request_id, mid as u32);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn all_rpc_status_values_roundtrip() {
|
||||
let statuses = [
|
||||
RpcStatus::Ok,
|
||||
RpcStatus::BadRequest,
|
||||
RpcStatus::Unauthorized,
|
||||
RpcStatus::Forbidden,
|
||||
RpcStatus::NotFound,
|
||||
RpcStatus::RateLimited,
|
||||
RpcStatus::DeadlineExceeded,
|
||||
RpcStatus::Unavailable,
|
||||
RpcStatus::Internal,
|
||||
RpcStatus::UnknownMethod,
|
||||
];
|
||||
for status in statuses {
|
||||
let frame = ResponseFrame {
|
||||
status: status as u8,
|
||||
request_id: 1,
|
||||
payload: Bytes::new(),
|
||||
};
|
||||
let encoded = frame.encode();
|
||||
let mut buf = BytesMut::from(encoded.as_ref());
|
||||
let decoded = ResponseFrame::decode(&mut buf).unwrap().unwrap();
|
||||
assert_eq!(decoded.rpc_status(), Some(status));
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn request_max_request_id() {
|
||||
let frame = RequestFrame {
|
||||
method_id: 1,
|
||||
request_id: u32::MAX,
|
||||
payload: Bytes::from_static(b"max-id"),
|
||||
};
|
||||
let encoded = frame.encode();
|
||||
let mut buf = BytesMut::from(encoded.as_ref());
|
||||
let decoded = RequestFrame::decode(&mut buf).unwrap().unwrap();
|
||||
assert_eq!(decoded.request_id, u32::MAX);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn multiple_frames_in_buffer() {
|
||||
// Two request frames concatenated in one buffer
|
||||
let f1 = RequestFrame {
|
||||
method_id: 1,
|
||||
request_id: 10,
|
||||
payload: Bytes::from_static(b"first"),
|
||||
};
|
||||
let f2 = RequestFrame {
|
||||
method_id: 2,
|
||||
request_id: 20,
|
||||
payload: Bytes::from_static(b"second"),
|
||||
};
|
||||
let mut buf = BytesMut::new();
|
||||
buf.extend_from_slice(&f1.encode());
|
||||
buf.extend_from_slice(&f2.encode());
|
||||
|
||||
let d1 = RequestFrame::decode(&mut buf).unwrap().unwrap();
|
||||
assert_eq!(d1.method_id, 1);
|
||||
assert_eq!(d1.payload, Bytes::from_static(b"first"));
|
||||
|
||||
let d2 = RequestFrame::decode(&mut buf).unwrap().unwrap();
|
||||
assert_eq!(d2.method_id, 2);
|
||||
assert_eq!(d2.payload, Bytes::from_static(b"second"));
|
||||
|
||||
assert!(buf.is_empty());
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user