use super::*;
fn replies() -> Vec<Reply> {
let node = NodeId::from_bytes([3; 16]);
let group = GroupId::from_bytes([4; 16]);
vec![
Reply::Broker(Broker {
name: "b1".into(),
id: node,
pid: 77,
uptime_ms: 1234,
max_clients: 64,
shards: 2,
brokers: vec![
("b1".into(), node),
("b2".into(), NodeId::from_bytes([5; 16])),
],
partitions: vec![PartitionName {
group,
topic: "orders".into(),
partition: 7,
shard: 1,
}],
}),
Reply::Counters(Counters {
uptime_ms: 99,
events: vec![
("io_sync".into(), EventKind::Count),
("peak".into(), EventKind::Peak),
],
totals: vec![10, u64::MAX],
threads: vec![
("ozzy/shard/0".into(), vec![4, 5]),
("ozzy/shard/0".into(), vec![6, 0]),
],
}),
Reply::Partitions(Partitions {
uptime_ms: 77,
missing_shards: 1,
rows: vec![
PartitionStatus {
group,
state: PartitionState::Normal,
ready: true,
disk_pending: false,
view: 3,
leader: Some(node),
accepted: 10,
committed: 9,
applied: 8,
pending_operations: 2,
pending_body_bytes: 4096,
validation_cycles: 1,
reclaimed_bytes: 1 << 30,
written: 10,
durable: 9,
leader_commit: 9,
followers: vec![
(NodeId::from_bytes([5; 16]), 9),
(NodeId::from_bytes([7; 16]), 4),
],
log: Some(PartitionLog {
first_offset: 100,
next_offset: 250,
producers: 3,
retention_age_millis: NonZeroU64::new(86_400_000),
retention_bytes: NonZeroU64::new(1 << 34),
policy_revision: 2,
segments: 5,
segment_bytes: 5 << 28,
dedup_entries: 1200,
dedup_limit: NonZeroU64::new(100_000),
groups: vec![GroupStatus {
id: ConsumerGroupId::from_bytes([8; 16]),
name: "billing".into(),
revision: 4,
members: vec![(ConsumerMemberId::from_bytes([9; 16]), node)],
}],
cursors: vec![
GroupCursor {
group: ConsumerGroupId::from_bytes([8; 16]),
member: Some(ConsumerMemberId::from_bytes([9; 16])),
offset: Some(200),
},
GroupCursor {
group: ConsumerGroupId::from_bytes([10; 16]),
member: None,
offset: None,
},
],
}),
},
PartitionStatus {
group: GroupId::from_bytes([6; 16]),
state: PartitionState::Local,
ready: false,
disk_pending: true,
view: 0,
leader: None,
accepted: 0,
committed: 0,
applied: 0,
pending_operations: 0,
pending_body_bytes: 0,
validation_cycles: 0,
reclaimed_bytes: 0,
written: 0,
durable: 0,
leader_commit: 0,
followers: Vec::new(),
log: None,
},
],
}),
Reply::Connections(Connections {
brokers: vec![(node, true), (NodeId::from_bytes([5; 16]), false)],
clients: 12,
}),
Reply::Error("unknown request".into()),
Reply::GroupDeleted(Some(ConsumerGroupId::from_bytes([11; 16]))),
Reply::GroupDeleted(None),
Reply::Logs(vec![
LogEntry {
uptime_ms: 0,
level: LogLevel::Info,
thread: "ozzy/admin".into(),
message: "admin deleted consumer group orders/billing".into(),
},
LogEntry {
uptime_ms: 5_000,
level: LogLevel::Error,
thread: "ozzy/shard/0".into(),
message: "replica journal fault".into(),
},
]),
]
}
#[test]
fn requests_round_trip_and_unknown_kinds_keep_their_id() {
for request in [
Request::Broker,
Request::Counters,
Request::Partitions,
Request::Connections,
Request::Logs,
Request::DeleteGroup {
topic: "orders".into(),
name: "billing".into(),
},
] {
let bytes = encode_request(42, &request).unwrap();
assert_eq!(decode_request(&bytes).unwrap(), (42, request));
}
let mut unknown = encode_request(43, &Request::Broker).unwrap();
unknown[0] = 9;
assert!(matches!(decode_request(&unknown), Err(CodecError::Command)));
assert_eq!(request_id(&unknown), Some(43));
assert!(decode_request(&unknown[..8]).is_err());
assert_eq!(request_id(&unknown[..8]), None);
unknown.push(0);
assert!(decode_request(&unknown).is_err());
}
#[test]
fn replies_round_trip_and_reject_every_truncation_and_trailing_byte() {
for reply in replies() {
let bytes = encode_reply(u64::MAX, &reply).unwrap();
assert_eq!(decode_reply(&bytes).unwrap(), (u64::MAX, reply));
for end in 0..bytes.len() {
assert!(decode_reply(&bytes[..end]).is_err(), "{end}");
}
let mut trailing = bytes;
trailing.push(0);
assert!(decode_reply(&trailing).is_err());
}
}
#[test]
fn counters_need_one_value_per_event() {
let Reply::Counters(mut counters) = replies().swap_remove(1) else {
unreachable!()
};
counters.threads[1].1.pop();
assert!(encode_reply(1, &Reply::Counters(counters)).is_err());
}
#[test]
fn oversized_counts_fail_before_allocating() {
let mut bytes = encode_reply(1, &replies().swap_remove(2)).unwrap();
bytes[21..25].copy_from_slice(&u32::MAX.to_be_bytes());
assert!(matches!(decode_reply(&bytes), Err(CodecError::Length)));
}
#[test]
fn unknown_flags_and_kinds_are_rejected() {
let bytes = encode_reply(1, &replies().swap_remove(2)).unwrap();
for (offset, value) in [(0, 3), (1, 0x80)] {
let mut bytes = bytes.clone();
bytes[9 + 8 + 4 + 4 + 16 + offset] = value;
assert!(matches!(decode_reply(&bytes), Err(CodecError::Profile)));
}
let mut bytes = encode_reply(1, &replies().swap_remove(1)).unwrap();
bytes[9 + 8 + 4 + 2 + 7] = 3;
assert!(matches!(decode_reply(&bytes), Err(CodecError::Profile)));
let mut bytes = encode_reply(1, &replies().swap_remove(3)).unwrap();
bytes[9 + 4 + 16] = 2;
assert!(matches!(decode_reply(&bytes), Err(CodecError::Profile)));
let mut bytes = encode_reply(1, &replies().swap_remove(4)).unwrap();
bytes[0] = 9;
assert!(matches!(decode_reply(&bytes), Err(CodecError::Command)));
}
#[test]
fn unknown_cursor_flags_are_rejected() {
let Reply::Partitions(mut partitions) = replies().swap_remove(2) else {
unreachable!()
};
partitions.rows.truncate(1);
let mut bytes = encode_reply(1, &Reply::Partitions(partitions)).unwrap();
let flags = bytes.len() - 1;
assert_eq!(bytes[flags], 0);
bytes[flags] = 4;
assert!(matches!(decode_reply(&bytes), Err(CodecError::Profile)));
}
#[test]
fn unset_limits_travel_as_zero() {
let Reply::Partitions(mut partitions) = replies().swap_remove(2) else {
unreachable!()
};
partitions.rows.truncate(1);
let log = partitions.rows[0].log.as_mut().unwrap();
log.retention_age_millis = None;
log.retention_bytes = None;
log.segments = 0;
log.segment_bytes = 0;
log.dedup_entries = 0;
log.dedup_limit = None;
let reply = Reply::Partitions(partitions);
let bytes = encode_reply(1, &reply).unwrap();
assert_eq!(decode_reply(&bytes).unwrap(), (1, reply));
}
#[test]
fn delete_group_requests_reject_truncation_and_long_names() {
let request = Request::DeleteGroup {
topic: "orders".into(),
name: "billing".into(),
};
let bytes = encode_request(7, &request).unwrap();
for end in 9..bytes.len() {
assert!(decode_request(&bytes[..end]).is_err(), "{end}");
}
let long = Request::DeleteGroup {
topic: "orders".into(),
name: "n".repeat(usize::from(u16::MAX) + 1),
};
assert!(encode_request(7, &long).is_err());
}
#[test]
fn unknown_log_levels_are_rejected() {
let reply = Reply::Logs(vec![LogEntry {
uptime_ms: 1,
level: LogLevel::Warn,
thread: String::new(),
message: String::new(),
}]);
let mut bytes = encode_reply(1, &reply).unwrap();
bytes[21] = 3;
assert!(matches!(decode_reply(&bytes), Err(CodecError::Profile)));
}
#[test]
fn unknown_deletion_flags_are_rejected() {
let mut bytes = encode_reply(1, &Reply::GroupDeleted(None)).unwrap();
bytes[9] = 2;
assert!(matches!(decode_reply(&bytes), Err(CodecError::Profile)));
}