use super::super::tests_support::{disconnect_reason_frames, round_trip};
use super::super::{decode, encode, encoded_len};
use crate::protocol::{
DELIVER_MESSAGE_ID_VERSION, DISCONNECT_REASON_BODY_MAX, DISCONNECT_REASON_BODY_VERSION,
DISCONNECT_REASON_NAME_MAX, DisconnectReason, Frame, FrameType, ProtocolError, ProtocolVersion,
REVOCATION_CLOSE_VERSION,
};
fn frame_bytes(stream: u32, body: &[u8]) -> Result<Vec<u8>, ProtocolError> {
let len = u32::try_from(body.len())
.map_err(|_| ProtocolError::codec("test body length cannot fit u32"))?;
let mut bytes = vec![u8::from(FrameType::DisconnectReason), 0];
bytes.extend_from_slice(&stream.to_be_bytes());
bytes.extend_from_slice(&len.to_be_bytes());
bytes.extend_from_slice(body);
Ok(bytes)
}
fn body(version: u8, code: u16, name: &[u8]) -> Result<Vec<u8>, ProtocolError> {
let len = u32::try_from(name.len())
.map_err(|_| ProtocolError::codec("test name length cannot fit u32"))?;
let mut body = vec![version];
body.extend_from_slice(&code.to_be_bytes());
body.extend_from_slice(&len.to_be_bytes());
body.extend_from_slice(name);
Ok(body)
}
fn codec_message(result: Result<(Frame, usize), ProtocolError>) -> Option<String> {
match result {
Err(ProtocolError::CodecError { message }) => message,
_ => None,
}
}
#[test]
fn every_registered_reason_round_trips_exactly() -> Result<(), ProtocolError> {
for frame in disconnect_reason_frames() {
assert_eq!(round_trip(&frame)?, frame);
}
for reason in DisconnectReason::ALL {
assert_eq!(
DisconnectReason::from_wire(reason.code(), reason.name()),
Some(reason)
);
assert!(reason.name().len() <= DISCONNECT_REASON_NAME_MAX);
assert!(
reason
.name()
.bytes()
.all(|byte| byte.is_ascii_lowercase() || byte == b'_')
);
}
Ok(())
}
#[test]
fn grant_revoked_encodes_to_its_pinned_bytes() -> Result<(), ProtocolError> {
let frame = Frame::DisconnectReason {
flags: 0,
reason: DisconnectReason::GrantRevoked,
};
let mut buffer = vec![0_u8; encoded_len(&frame)?];
let written = encode(&frame, &mut buffer)?;
let expected = frame_bytes(0, &body(1, 0x0001, b"grant_revoked")?)?;
assert_eq!(buffer.get(..written), Some(expected.as_slice()));
assert_eq!(written, 10 + 1 + 2 + 4 + 13);
assert!(
written
.checked_sub(10)
.is_some_and(|len| len <= DISCONNECT_REASON_BODY_MAX)
);
assert_eq!(DISCONNECT_REASON_BODY_VERSION, 1);
Ok(())
}
#[test]
fn the_payload_free_disconnect_keeps_its_bytes() -> Result<(), ProtocolError> {
let frame = Frame::Disconnect { flags: 0 };
let mut buffer = vec![0_u8; encoded_len(&frame)?];
let written = encode(&frame, &mut buffer)?;
assert_eq!(
buffer.get(..written),
Some(&[0x04_u8, 0, 0, 0, 0, 0, 0, 0, 0, 0][..])
);
Ok(())
}
#[test]
fn an_old_disconnect_with_a_trailing_reason_is_refused() -> Result<(), ProtocolError> {
let reason = body(1, 0x0001, b"grant_revoked")?;
let len = u32::try_from(reason.len())
.map_err(|_| ProtocolError::codec("test body length cannot fit u32"))?;
let mut bytes = vec![u8::from(FrameType::Disconnect), 0, 0, 0, 0, 0];
bytes.extend_from_slice(&len.to_be_bytes());
bytes.extend_from_slice(&reason);
assert!(decode(&bytes).is_err());
Ok(())
}
#[test]
fn an_unknown_body_version_is_refused_by_name() -> Result<(), ProtocolError> {
for version in [0_u8, 2, 0xFF] {
let bytes = frame_bytes(0, &body(version, 0x0001, b"grant_revoked")?)?;
assert_eq!(
codec_message(decode(&bytes)).as_deref(),
Some("disconnect reason body version is not supported")
);
}
Ok(())
}
#[test]
fn an_unknown_or_mismatched_reason_is_refused_by_name() -> Result<(), ProtocolError> {
for (code, name) in [
(0x0003_u16, &b"grant_revoked"[..]),
(0x0002, b"grant_revoked"),
(0x0001, b"revocation_log_reset"),
(0x0001, b"Grant_Revoked"),
(0x0001, b"grant_revoked "),
(0x0000, b""),
(0xFFFF, b"future_reason"),
] {
let bytes = frame_bytes(0, &body(1, code, name)?)?;
assert_eq!(
codec_message(decode(&bytes)).as_deref(),
Some("disconnect reason code and name are not one registered reason")
);
}
Ok(())
}
#[test]
fn a_name_over_its_bound_is_refused_by_name() -> Result<(), ProtocolError> {
let at_bound = vec![b'a'; DISCONNECT_REASON_NAME_MAX];
let over_len = DISCONNECT_REASON_NAME_MAX
.checked_add(1)
.ok_or_else(|| ProtocolError::codec("bound overflow"))?;
let over = vec![b'a'; over_len];
let at_bound_message = codec_message(decode(&frame_bytes(0, &body(1, 1, &at_bound)?)?));
assert_eq!(
at_bound_message.as_deref(),
Some("disconnect reason code and name are not one registered reason")
);
let over_message = codec_message(decode(&frame_bytes(0, &body(1, 1, &over)?)?));
assert_eq!(
over_message.as_deref(),
Some("disconnect reason name exceeds its bound")
);
Ok(())
}
#[test]
fn a_truncated_or_padded_body_is_refused() -> Result<(), Box<dyn std::error::Error>> {
let whole = body(1, 0x0001, b"grant_revoked")?;
for cut in 0..whole.len() {
let truncated = whole.get(..cut).ok_or("cut within body")?;
assert!(decode(&frame_bytes(0, truncated)?).is_err(), "cut at {cut}");
}
let mut padded = whole;
padded.push(0);
assert!(decode(&frame_bytes(0, &padded)?).is_err());
Ok(())
}
#[test]
fn the_reason_bearing_close_lives_on_the_control_stream() -> Result<(), ProtocolError> {
let bytes = frame_bytes(1, &body(1, 0x0001, b"grant_revoked")?)?;
assert!(matches!(
decode(&bytes),
Err(ProtocolError::InvalidStream { .. })
));
assert!(FrameType::DisconnectReason.is_control());
Ok(())
}
#[test]
fn the_close_version_follows_every_earlier_version() {
assert_eq!(REVOCATION_CLOSE_VERSION, ProtocolVersion::new(1, 3));
assert!(REVOCATION_CLOSE_VERSION > DELIVER_MESSAGE_ID_VERSION);
}
#[test]
fn the_reason_debug_names_the_reason_only() {
let frame = Frame::DisconnectReason {
flags: 0,
reason: DisconnectReason::GrantRevoked,
};
assert_eq!(
format!("{frame:?}"),
"DisconnectReason { flags: 0, reason: GrantRevoked }"
);
}