use lora_packet::{Direction, Error, LoraPacket, MType, Payload, RejoinRequest};
fn hex_to_vec(s: &str) -> Vec<u8> {
hex::decode(s).expect("valid hex string")
}
#[test]
fn empty_buffer_returns_too_short() {
let err = LoraPacket::from_wire(&[]).unwrap_err();
match err {
Error::TooShort { expected, got } => {
assert_eq!(expected, 5);
assert_eq!(got, 0);
}
other => panic!("expected TooShort, got {other:?}"),
}
}
#[test]
fn single_byte_returns_too_short() {
let err = LoraPacket::from_wire(&[0x40]).unwrap_err();
assert!(matches!(err, Error::TooShort { expected: 5, got: 1 }));
}
#[test]
fn two_byte_buffer_returns_too_short() {
let err = LoraPacket::from_wire(&[0xC0, 0x00]).unwrap_err();
assert!(matches!(err, Error::TooShort { expected: 5, got: 2 }));
}
#[test]
fn three_byte_buffer_returns_too_short() {
let err = LoraPacket::from_wire(&[0x40, 0x00, 0x00]).unwrap_err();
assert!(matches!(err, Error::TooShort { expected: 5, got: 3 }));
}
#[test]
fn four_byte_buffer_returns_too_short() {
let err = LoraPacket::from_wire(&[1, 2, 3, 4]).unwrap_err();
assert!(matches!(err, Error::TooShort { expected: 5, got: 4 }));
}
#[test]
fn five_byte_buffer_mhdr_plus_mic_is_proprietary_only() {
let bytes = [0xE0, 0xAA, 0xBB, 0xCC, 0xDD];
let p = LoraPacket::from_wire(&bytes).unwrap();
assert_eq!(p.m_type(), MType::Proprietary);
assert_eq!(p.mic, [0xAA, 0xBB, 0xCC, 0xDD]);
match &p.payload {
Payload::Proprietary(body) => assert!(body.is_empty()),
other => panic!("expected Proprietary, got {other:?}"),
}
}
#[test]
fn five_byte_data_uplink_has_no_body_room() {
let bytes = [0x40, 0xAA, 0xBB, 0xCC, 0xDD];
let err = LoraPacket::from_wire(&bytes).unwrap_err();
assert!(matches!(err, Error::TooShort { expected: 7, got: 0 }));
}
#[test]
fn five_byte_join_request_has_no_body_room() {
let bytes = [0x00, 0xAA, 0xBB, 0xCC, 0xDD];
let err = LoraPacket::from_wire(&bytes).unwrap_err();
assert!(matches!(err, Error::TooShort { expected: 18, got: 0 }));
}
#[test]
fn five_byte_rejoin_has_no_type_byte() {
let bytes = [0xC0, 0xAA, 0xBB, 0xCC, 0xDD];
let err = LoraPacket::from_wire(&bytes).unwrap_err();
assert!(matches!(err, Error::TooShort { expected: 1, got: 0 }));
}
#[test]
fn five_byte_join_accept_rejects_with_other_error() {
let bytes = [0x20, 0xAA, 0xBB, 0xCC, 0xDD];
let err = LoraPacket::from_wire(&bytes).unwrap_err();
assert!(matches!(err, Error::Other(_)));
}
#[test]
fn mtype_000_join_request_minimum_body() {
let mut bytes = vec![0x00];
bytes.extend_from_slice(&[0u8; 18]); bytes.extend_from_slice(&[0xDE, 0xAD, 0xBE, 0xEF]);
let p = LoraPacket::from_wire(&bytes).unwrap();
assert_eq!(p.m_type(), MType::JoinRequest);
assert!(p.is_join_request());
}
#[test]
fn mtype_001_join_accept_via_from_wire_rejected() {
let bytes = hex_to_vec("20010203040506070809100001deadbeef");
let err = LoraPacket::from_wire(&bytes).unwrap_err();
match err {
Error::Other(msg) => assert!(msg.contains("decrypt")),
other => panic!("expected Other(decrypt msg), got {other:?}"),
}
}
#[test]
fn mtype_010_unconfirmed_data_up_parses() {
let bytes = hex_to_vec("40f17dbe4900020001954378762b11ff0d");
let p = LoraPacket::from_wire(&bytes).unwrap();
assert_eq!(p.m_type(), MType::UnconfirmedDataUp);
let d = p.as_data().unwrap();
assert_eq!(d.direction, Direction::Uplink);
assert!(!d.confirmed);
}
#[test]
fn mtype_011_unconfirmed_data_down_parses() {
let bytes = hex_to_vec("60f17dbe4920020001f9d65d27");
let p = LoraPacket::from_wire(&bytes).unwrap();
assert_eq!(p.m_type(), MType::UnconfirmedDataDown);
let d = p.as_data().unwrap();
assert_eq!(d.direction, Direction::Downlink);
assert!(!d.confirmed);
}
#[test]
fn mtype_100_confirmed_data_up_parses() {
let mut bytes = vec![0x80];
bytes.extend_from_slice(&[0xf1, 0x7d, 0xbe, 0x49, 0x00, 0x02, 0x00]); bytes.extend_from_slice(&[0xAA, 0xBB, 0xCC, 0xDD]); let p = LoraPacket::from_wire(&bytes).unwrap();
assert_eq!(p.m_type(), MType::ConfirmedDataUp);
let d = p.as_data().unwrap();
assert_eq!(d.direction, Direction::Uplink);
assert!(d.confirmed);
}
#[test]
fn mtype_101_confirmed_data_down_parses() {
let mut bytes = vec![0xA0];
bytes.extend_from_slice(&[0xf1, 0x7d, 0xbe, 0x49, 0x00, 0x02, 0x00]);
bytes.extend_from_slice(&[0xAA, 0xBB, 0xCC, 0xDD]);
let p = LoraPacket::from_wire(&bytes).unwrap();
assert_eq!(p.m_type(), MType::ConfirmedDataDown);
let d = p.as_data().unwrap();
assert_eq!(d.direction, Direction::Downlink);
assert!(d.confirmed);
}
#[test]
fn mtype_110_rejoin_request_parses() {
let bytes = hex_to_vec("c0000102030405060708090a0b0c0ddeadbeef");
let p = LoraPacket::from_wire(&bytes).unwrap();
assert_eq!(p.m_type(), MType::RejoinRequest);
assert!(p.is_rejoin_request());
}
#[test]
fn mtype_111_proprietary_parses() {
let bytes = hex_to_vec("e0deadbeefcafe11223344");
let p = LoraPacket::from_wire(&bytes).unwrap();
assert_eq!(p.m_type(), MType::Proprietary);
match &p.payload {
Payload::Proprietary(body) => assert_eq!(body.as_slice(), &[0xde, 0xad, 0xbe, 0xef, 0xca, 0xfe]),
other => panic!("expected Proprietary, got {other:?}"),
}
}
#[test]
fn data_fopts_len_exceeds_available_body() {
let mut bytes = vec![0x40]; bytes.extend_from_slice(&[0xf1, 0x7d, 0xbe, 0x49]); bytes.push(0x0F); bytes.extend_from_slice(&[0x02, 0x00]); bytes.extend_from_slice(&[0xDE, 0xAD, 0xBE, 0xEF]); let err = LoraPacket::from_wire(&bytes).unwrap_err();
match err {
Error::TooShort { expected, got } => {
assert_eq!(expected, 22);
assert_eq!(got, 7);
}
other => panic!("expected TooShort, got {other:?}"),
}
}
#[test]
fn data_fopts_len_one_byte_short() {
let mut bytes = vec![0x40];
bytes.extend_from_slice(&[0xf1, 0x7d, 0xbe, 0x49]);
bytes.push(0x05); bytes.extend_from_slice(&[0x02, 0x00]);
bytes.extend_from_slice(&[0xAA, 0xBB, 0xCC, 0xDD]); bytes.extend_from_slice(&[0xDE, 0xAD, 0xBE, 0xEF]); let err = LoraPacket::from_wire(&bytes).unwrap_err();
match err {
Error::TooShort { expected, got } => {
assert_eq!(expected, 12); assert_eq!(got, 11); }
other => panic!("expected TooShort, got {other:?}"),
}
}
#[test]
fn data_fopts_len_15_with_exactly_15_fopts_no_fport() {
let mut bytes = vec![0x40];
bytes.extend_from_slice(&[0xf1, 0x7d, 0xbe, 0x49]); bytes.push(0x0F); bytes.extend_from_slice(&[0x02, 0x00]); bytes.extend_from_slice(&[0x11; 15]); bytes.extend_from_slice(&[0xDE, 0xAD, 0xBE, 0xEF]); let p = LoraPacket::from_wire(&bytes).unwrap();
let d = p.as_data().unwrap();
assert_eq!(d.f_ctrl.f_opts_len(), 15);
assert_eq!(d.f_opts.len(), 15);
assert_eq!(d.f_opts, vec![0x11; 15]);
assert_eq!(d.f_port, None);
assert_eq!(d.frm_payload, None);
assert_eq!(p.mic, [0xDE, 0xAD, 0xBE, 0xEF]);
}
#[test]
fn data_fopts_len_15_with_15_fopts_and_fport_only() {
let mut bytes = vec![0x40];
bytes.extend_from_slice(&[0xf1, 0x7d, 0xbe, 0x49]);
bytes.push(0x0F);
bytes.extend_from_slice(&[0x02, 0x00]);
bytes.extend_from_slice(&[0x22; 15]);
bytes.push(0x07); bytes.extend_from_slice(&[0xDE, 0xAD, 0xBE, 0xEF]); let p = LoraPacket::from_wire(&bytes).unwrap();
let d = p.as_data().unwrap();
assert_eq!(d.f_opts.len(), 15);
assert_eq!(d.f_port, Some(0x07));
assert_eq!(d.frm_payload.as_deref(), Some(&[][..]));
}
#[test]
fn data_fopts_len_zero_no_fport_no_payload() {
let mut bytes = vec![0x40];
bytes.extend_from_slice(&[0xf1, 0x7d, 0xbe, 0x49]); bytes.push(0x00); bytes.extend_from_slice(&[0x01, 0x00]); bytes.extend_from_slice(&[0xDE, 0xAD, 0xBE, 0xEF]); let p = LoraPacket::from_wire(&bytes).unwrap();
let d = p.as_data().unwrap();
assert!(d.f_opts.is_empty());
assert_eq!(d.f_port, None);
assert_eq!(d.frm_payload, None);
}
#[test]
fn data_fport_0_with_fopts_present_parser_accepts() {
let mut bytes = vec![0x40];
bytes.extend_from_slice(&[0xf1, 0x7d, 0xbe, 0x49]); bytes.push(0x03); bytes.extend_from_slice(&[0x02, 0x00]); bytes.extend_from_slice(&[0xAA, 0xBB, 0xCC]); bytes.push(0x00); bytes.extend_from_slice(&[0x11, 0x22]); bytes.extend_from_slice(&[0xDE, 0xAD, 0xBE, 0xEF]); let p = LoraPacket::from_wire(&bytes).unwrap();
let d = p.as_data().unwrap();
assert_eq!(d.f_port, Some(0));
assert_eq!(d.f_opts, vec![0xAA, 0xBB, 0xCC]);
assert_eq!(d.frm_payload.as_deref(), Some(&[0x11, 0x22][..]));
}
#[test]
fn join_request_body_17_bytes_rejected() {
let mut bytes = vec![0x00];
bytes.extend_from_slice(&[0u8; 17]);
bytes.extend_from_slice(&[0xDE, 0xAD, 0xBE, 0xEF]);
let err = LoraPacket::from_wire(&bytes).unwrap_err();
match err {
Error::TooShort { expected, got } => {
assert_eq!(expected, 18);
assert_eq!(got, 17);
}
other => panic!("expected TooShort, got {other:?}"),
}
}
#[test]
fn join_request_body_19_bytes_rejected() {
let mut bytes = vec![0x00];
bytes.extend_from_slice(&[0u8; 19]);
bytes.extend_from_slice(&[0xDE, 0xAD, 0xBE, 0xEF]);
let err = LoraPacket::from_wire(&bytes).unwrap_err();
match err {
Error::TooShort { expected, got } => {
assert_eq!(expected, 18);
assert_eq!(got, 19);
}
other => panic!("expected TooShort, got {other:?}"),
}
}
#[test]
fn join_request_body_0_bytes_rejected() {
let bytes = vec![0x00, 0xDE, 0xAD, 0xBE, 0xEF];
let err = LoraPacket::from_wire(&bytes).unwrap_err();
assert!(matches!(err, Error::TooShort { expected: 18, got: 0 }));
}
#[test]
fn join_request_exact_18_byte_body_parses() {
let mut bytes = vec![0x00];
bytes.extend_from_slice(&[0u8; 18]);
bytes.extend_from_slice(&[0xDE, 0xAD, 0xBE, 0xEF]);
let p = LoraPacket::from_wire(&bytes).unwrap();
let jr = p.as_join_request().unwrap();
assert_eq!(jr.join_eui.as_bytes(), &[0u8; 8]);
assert_eq!(jr.dev_eui.as_bytes(), &[0u8; 8]);
assert_eq!(jr.dev_nonce.as_bytes(), &[0u8; 2]);
}
#[test]
fn rejoin_type_3_invalid() {
let bytes = hex_to_vec("c0030102030405060708090a0b0c0ddeadbeef");
let err = LoraPacket::from_wire(&bytes).unwrap_err();
assert!(matches!(err, Error::InvalidRejoinType(3)));
}
#[test]
fn rejoin_type_4_invalid() {
let bytes = hex_to_vec("c0040102030405060708090a0b0c0ddeadbeef");
let err = LoraPacket::from_wire(&bytes).unwrap_err();
assert!(matches!(err, Error::InvalidRejoinType(4)));
}
#[test]
fn rejoin_type_5_invalid() {
let bytes = hex_to_vec("c0050102030405060708090a0b0c0ddeadbeef");
let err = LoraPacket::from_wire(&bytes).unwrap_err();
assert!(matches!(err, Error::InvalidRejoinType(5)));
}
#[test]
fn rejoin_type_127_invalid_midrange() {
let mut bytes = vec![0xC0, 127];
bytes.extend_from_slice(&[0u8; 13]); bytes.extend_from_slice(&[0xDE, 0xAD, 0xBE, 0xEF]);
let err = LoraPacket::from_wire(&bytes).unwrap_err();
assert!(matches!(err, Error::InvalidRejoinType(127)));
}
#[test]
fn rejoin_type_254_invalid() {
let mut bytes = vec![0xC0, 254];
bytes.extend_from_slice(&[0u8; 13]);
bytes.extend_from_slice(&[0xDE, 0xAD, 0xBE, 0xEF]);
let err = LoraPacket::from_wire(&bytes).unwrap_err();
assert!(matches!(err, Error::InvalidRejoinType(254)));
}
#[test]
fn rejoin_type_255_invalid() {
let mut bytes = vec![0xC0, 255];
bytes.extend_from_slice(&[0u8; 13]);
bytes.extend_from_slice(&[0xDE, 0xAD, 0xBE, 0xEF]);
let err = LoraPacket::from_wire(&bytes).unwrap_err();
assert!(matches!(err, Error::InvalidRejoinType(255)));
}
#[test]
fn rejoin_all_invalid_types_3_through_255() {
for t in 3u8..=255 {
let mut bytes = vec![0xC0, t];
bytes.extend_from_slice(&[0u8; 13]);
bytes.extend_from_slice(&[0xDE, 0xAD, 0xBE, 0xEF]);
let err = LoraPacket::from_wire(&bytes).unwrap_err();
match err {
Error::InvalidRejoinType(got) => assert_eq!(got, t, "type byte {t} surfaced as {got}"),
other => panic!("type byte {t} produced {other:?}"),
}
}
}
#[test]
fn rejoin_type_0_short_body() {
let mut bytes = vec![0xC0, 0x00];
bytes.extend_from_slice(&[0u8; 12]); bytes.extend_from_slice(&[0xDE, 0xAD, 0xBE, 0xEF]);
let err = LoraPacket::from_wire(&bytes).unwrap_err();
match err {
Error::TooShort { expected, got } => {
assert_eq!(expected, 14);
assert_eq!(got, 13);
}
other => panic!("expected TooShort, got {other:?}"),
}
}
#[test]
fn rejoin_type_0_long_body() {
let mut bytes = vec![0xC0, 0x00];
bytes.extend_from_slice(&[0u8; 14]); bytes.extend_from_slice(&[0xDE, 0xAD, 0xBE, 0xEF]);
let err = LoraPacket::from_wire(&bytes).unwrap_err();
match err {
Error::TooShort { expected, got } => {
assert_eq!(expected, 14);
assert_eq!(got, 15);
}
other => panic!("expected TooShort, got {other:?}"),
}
}
#[test]
fn rejoin_type_1_short_body() {
let mut bytes = vec![0xC0, 0x01];
bytes.extend_from_slice(&[0u8; 17]); bytes.extend_from_slice(&[0xDE, 0xAD, 0xBE, 0xEF]);
let err = LoraPacket::from_wire(&bytes).unwrap_err();
match err {
Error::TooShort { expected, got } => {
assert_eq!(expected, 19);
assert_eq!(got, 18);
}
other => panic!("expected TooShort, got {other:?}"),
}
}
#[test]
fn rejoin_type_2_short_body() {
let mut bytes = vec![0xC0, 0x02];
bytes.extend_from_slice(&[0u8; 12]); bytes.extend_from_slice(&[0xDE, 0xAD, 0xBE, 0xEF]);
let err = LoraPacket::from_wire(&bytes).unwrap_err();
match err {
Error::TooShort { expected, got } => {
assert_eq!(expected, 14);
assert_eq!(got, 13);
}
other => panic!("expected TooShort, got {other:?}"),
}
}
#[test]
fn rejoin_type_2_parses_into_type2_variant() {
let bytes = hex_to_vec("c0020102030405060708090a0b0c0ddeadbeef");
let p = LoraPacket::from_wire(&bytes).unwrap();
match p.as_rejoin_request().unwrap() {
RejoinRequest::Type2 { .. } => {}
other => panic!("expected Type2, got {other:?}"),
}
}
#[test]
fn all_zero_buffer_5_bytes_parses_as_join_request_too_short() {
let bytes = [0u8; 5];
let err = LoraPacket::from_wire(&bytes).unwrap_err();
assert!(matches!(err, Error::TooShort { expected: 18, got: 0 }));
}
#[test]
fn all_zero_buffer_23_bytes_parses_as_join_request() {
let bytes = [0u8; 23];
let p = LoraPacket::from_wire(&bytes).unwrap();
let jr = p.as_join_request().unwrap();
assert_eq!(jr.join_eui.as_bytes(), &[0u8; 8]);
assert_eq!(jr.dev_eui.as_bytes(), &[0u8; 8]);
assert_eq!(jr.dev_nonce.as_bytes(), &[0u8; 2]);
assert_eq!(p.mic, [0, 0, 0, 0]);
}
#[test]
fn all_ff_buffer_5_bytes_parses_as_proprietary() {
let bytes = [0xFFu8; 5];
let p = LoraPacket::from_wire(&bytes).unwrap();
assert_eq!(p.m_type(), MType::Proprietary);
assert_eq!(p.mic, [0xFF; 4]);
match &p.payload {
Payload::Proprietary(body) => assert!(body.is_empty()),
other => panic!("expected Proprietary, got {other:?}"),
}
}
#[test]
fn all_ff_buffer_32_bytes_parses_as_proprietary() {
let bytes = [0xFFu8; 32];
let p = LoraPacket::from_wire(&bytes).unwrap();
match &p.payload {
Payload::Proprietary(body) => {
assert_eq!(body.len(), 27);
assert!(body.iter().all(|b| *b == 0xFF));
}
other => panic!("expected Proprietary, got {other:?}"),
}
assert_eq!(p.mic, [0xFF; 4]);
}
#[test]
fn alternating_aa_buffer_parses_as_confirmed_data_down() {
let bytes = [0xAAu8; 22];
let p = LoraPacket::from_wire(&bytes).unwrap();
assert_eq!(p.m_type(), MType::ConfirmedDataDown);
let d = p.as_data().unwrap();
assert_eq!(d.direction, Direction::Downlink);
assert!(d.confirmed);
assert_eq!(d.f_ctrl.f_opts_len(), 10);
assert_eq!(d.f_opts.len(), 10);
assert_eq!(d.f_opts, vec![0xAA; 10]);
assert_eq!(d.f_port, None);
assert_eq!(d.frm_payload, None);
assert_eq!(p.mhdr.as_byte() & 0b11, 0b10);
assert_eq!(p.mic, [0xAA; 4]);
}
#[test]
fn alternating_55_buffer_parses_as_unconfirmed_data_up_with_unaligned_fopts() {
let bytes = [0x55u8; 19];
let p = LoraPacket::from_wire(&bytes).unwrap();
assert_eq!(p.m_type(), MType::UnconfirmedDataUp);
let d = p.as_data().unwrap();
assert_eq!(d.f_ctrl.f_opts_len(), 5);
assert_eq!(d.f_opts, vec![0x55; 5]);
assert_eq!(d.f_port, Some(0x55));
assert_eq!(d.frm_payload.as_deref(), Some(&[0x55][..]));
}
#[test]
fn major_version_nonzero_accepted_by_parser() {
let mut bytes = vec![0x43];
bytes.extend_from_slice(&[0xf1, 0x7d, 0xbe, 0x49, 0x00, 0x02, 0x00]);
bytes.extend_from_slice(&[0xDE, 0xAD, 0xBE, 0xEF]);
let p = LoraPacket::from_wire(&bytes).unwrap();
assert_eq!(p.mhdr.as_byte() & 0b11, 0b11);
assert_eq!(p.m_type(), MType::UnconfirmedDataUp);
}
#[test]
fn data_frame_with_large_frm_payload_parses() {
let mut bytes = vec![0x40];
bytes.extend_from_slice(&[0xf1, 0x7d, 0xbe, 0x49, 0x00, 0x02, 0x00]); bytes.push(0x01); bytes.extend_from_slice(&vec![0xCC; 222]); bytes.extend_from_slice(&[0xDE, 0xAD, 0xBE, 0xEF]); let p = LoraPacket::from_wire(&bytes).unwrap();
let d = p.as_data().unwrap();
assert_eq!(d.f_port, Some(0x01));
assert_eq!(d.frm_payload.as_deref().unwrap().len(), 222);
assert!(d.frm_payload.as_deref().unwrap().iter().all(|b| *b == 0xCC));
}
#[test]
fn proprietary_with_oversized_body_rejected() {
let mut bytes = vec![0xE0];
bytes.extend_from_slice(&vec![0x77; 4080]);
bytes.extend_from_slice(&[0xDE, 0xAD, 0xBE, 0xEF]);
let err = LoraPacket::from_wire(&bytes).unwrap_err();
assert!(matches!(err, lora_packet::Error::TooLong { got: 4085 }));
}
#[test]
fn proprietary_at_max_size_parses() {
let mut bytes = vec![0xE0];
bytes.extend_from_slice(&vec![0x77; 251]);
bytes.extend_from_slice(&[0xDE, 0xAD, 0xBE, 0xEF]);
assert_eq!(bytes.len(), 256);
let p = LoraPacket::from_wire(&bytes).unwrap();
assert_eq!(p.m_type(), MType::Proprietary);
match &p.payload {
Payload::Proprietary(body) => {
assert_eq!(body.len(), 251);
assert!(body.iter().all(|b| *b == 0x77));
}
other => panic!("expected Proprietary, got {other:?}"),
}
assert_eq!(p.mic, [0xDE, 0xAD, 0xBE, 0xEF]);
}
#[test]
fn data_round_trip_preserves_bytes_with_max_fopts() {
let mut bytes = vec![0x40];
bytes.extend_from_slice(&[0xf1, 0x7d, 0xbe, 0x49]);
bytes.push(0x0F);
bytes.extend_from_slice(&[0x02, 0x00]);
bytes.extend_from_slice(&[0x11; 15]);
bytes.push(0x07);
bytes.extend_from_slice(&[0x99, 0xAA, 0xBB]); bytes.extend_from_slice(&[0xDE, 0xAD, 0xBE, 0xEF]);
let p = LoraPacket::from_wire(&bytes).unwrap();
assert_eq!(p.to_wire(), bytes);
}
#[test]
fn proprietary_round_trip_preserves_bytes() {
let bytes = hex_to_vec("e0deadbeefcafe11223344");
let p = LoraPacket::from_wire(&bytes).unwrap();
assert_eq!(p.to_wire(), bytes);
}