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:
2026-03-08 18:07:43 +01:00
parent e4c5868b31
commit 872695e5f1
8 changed files with 1114 additions and 0 deletions

View File

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