use bacnet_encoding::npdu::{Npdu, NpduAddress, decode_npdu};
use bacnet_npdu::{NetworkMessageType, NpduLimits};
use bacnet_types::MacAddr;
use bytes::Bytes;
use super::SendRequest;
use super::envelope_harness::Harness;
const SENDER: [u8; 6] = [10, 0, 0, 9, 0xBA, 0xC0];
fn address(network: u16, mac: &[u8]) -> NpduAddress {
NpduAddress {
network,
mac_address: MacAddr::from_slice(mac),
}
}
fn npdu_of(len: usize, network: u16, mac: &[u8]) -> Npdu {
let header = 6 + mac.len();
Npdu {
destination: Some(address(network, mac)),
hop_count: 255,
payload: Bytes::from(vec![0x10; len - header]),
..Npdu::default()
}
}
fn reject_on(h: &mut Harness, port: usize) -> (u8, u16, MacAddr) {
let sent = h.drain(port);
let [SendRequest::Unicast { npdu, mac, .. }] = &sent[..] else {
panic!("expected one reject on port {port}, got {sent:?}");
};
let npdu = decode_npdu(npdu.clone()).unwrap();
assert_eq!(npdu.message_type, Some(NetworkMessageType::from_raw(0x03)));
let [reason, high, low] = npdu.payload[..] else {
panic!("a reject's data is three octets");
};
(reason, u16::from_be_bytes([high, low]), mac.clone())
}
fn lengths_on(h: &mut Harness, port: usize) -> Vec<usize> {
h.drain(port)
.into_iter()
.map(|sent| match sent {
SendRequest::Unicast { npdu, .. } | SendRequest::Broadcast { npdu, .. } => npdu.len(),
})
.collect()
}
#[tokio::test]
async fn r1_an_npdu_too_long_for_an_mstp_port_draws_reason_4() {
for limit in [1497, 501] {
let mut h = Harness::with_limits(
&[1000, 2000],
&[NpduLimits::STANDARD, NpduLimits::both(limit)],
);
let longest = usize::from(limit) - 4;
h.dispatch(h.ctx(0, &SENDER, npdu_of(longest + 1, 2000, &[5])))
.await;
assert_eq!(
reject_on(&mut h, 0),
(4, 2000, MacAddr::from_slice(&SENDER))
);
h.assert_quiet();
let counters = h.counters();
assert_eq!((counters.egress_too_long, counters.rejects), (1, 1));
h.dispatch(h.ctx(0, &SENDER, npdu_of(longest, 2000, &[5])))
.await;
assert_eq!(lengths_on(&mut h, 1), [usize::from(limit)]);
h.assert_quiet();
h.dispatch(h.ctx(0, &SENDER, npdu_of(longest, 2000, &[])))
.await;
assert_eq!(
reject_on(&mut h, 0),
(4, 2000, MacAddr::from_slice(&SENDER))
);
h.assert_quiet();
assert_eq!(h.counters().egress_too_long, 2);
}
}
#[tokio::test]
async fn r1_a_port_reporting_under_64_octets_carries_64() {
let mut h = Harness::with_limits(&[1000, 2000], &[NpduLimits::STANDARD, NpduLimits::both(8)]);
h.dispatch(h.ctx(0, &SENDER, npdu_of(60, 2000, &[5]))).await;
assert_eq!(lengths_on(&mut h, 1), [64]);
h.dispatch(h.ctx(0, &SENDER, npdu_of(61, 2000, &[5]))).await;
assert_eq!(
reject_on(&mut h, 0),
(4, 2000, MacAddr::from_slice(&SENDER))
);
h.assert_quiet();
}
#[tokio::test]
async fn r1_a_global_broadcast_skips_a_port_too_short_for_it() {
let mut h = Harness::with_limits(
&[1000, 2000, 3000],
&[
NpduLimits::STANDARD,
NpduLimits::both(501),
NpduLimits::STANDARD,
],
);
h.dispatch(h.ctx(0, &SENDER, npdu_of(600, 0xFFFF, &[])))
.await;
assert_eq!(lengths_on(&mut h, 2), [609]);
assert!(h.apdus.try_recv().is_ok(), "delivered locally");
h.assert_quiet();
let counters = h.counters();
assert_eq!((counters.egress_too_long, counters.rejects), (1, 0));
}
#[tokio::test]
async fn r2_a_reason_4_reject_is_passed_on_and_keeps_the_route() {
let mut h = Harness::two_port();
h.learned(3000, 1, &[9]);
let before = h.table().lookup(3000);
let reject = Npdu {
message_type: Some(NetworkMessageType::from_raw(0x03)),
destination: Some(address(1000, &SENDER)),
hop_count: 255,
payload: Bytes::from_static(&[4, 0x0B, 0xB8]),
..Npdu::default()
};
h.dispatch(h.ctx(1, &[9], reject)).await;
let sent = h.drain(0);
let [SendRequest::Unicast { npdu, mac, .. }] = &sent[..] else {
panic!("expected the reject passed on to port 0, got {sent:?}");
};
assert_eq!(mac.as_slice(), SENDER);
assert!(npdu.ends_with(&[0x03, 4, 0x0B, 0xB8]));
h.assert_quiet();
assert_eq!(h.table().lookup(3000), before);
let counters = h.table().claim_counters();
assert_eq!(
(
counters.reject_applied,
counters.reject_dampened,
counters.reject_kept
),
(0, 0, 1)
);
}
#[tokio::test]
async fn r4_the_unscoped_answer_leaves_out_unreachable_networks() {
let mut h = Harness::two_port();
h.learned(3001, 1, &[9]);
h.learned(2500, 1, &[9]);
h.learned(3000, 1, &[8]);
h.learned(4000, 0, &[7]);
let busy_until = h.now() + bacnet_router_core::Duration::from_secs(30);
{
let mut table = h.table();
table.mark_unreachable(3001);
table.mark_busy(2500, busy_until);
}
let who_is_all = Npdu {
message_type: Some(NetworkMessageType::from_raw(0x00)),
..Npdu::default()
};
h.dispatch(h.ctx(0, &SENDER, who_is_all)).await;
let sent = h.drain(0);
let [SendRequest::Broadcast { npdu, .. }] = &sent[..] else {
panic!("expected one I-Am-Router broadcast on port 0, got {sent:?}");
};
assert_eq!(
npdu.as_ref(),
[0x01, 0x80, 0x01, 0x07, 0xD0, 0x09, 0xC4, 0x0B, 0xB8]
);
h.assert_quiet();
}
fn who_is(network: u16) -> Npdu {
Npdu {
message_type: Some(NetworkMessageType::from_raw(0x00)),
payload: Bytes::copy_from_slice(&network.to_be_bytes()),
..Npdu::default()
}
}
#[tokio::test]
async fn r5_a_network_behind_the_arrival_port_is_neither_answered_nor_searched() {
let mut h = Harness::two_port();
h.learned(3000, 0, &[9]);
for network in [3000, 1000] {
h.dispatch(h.ctx(0, &SENDER, who_is(network))).await;
h.assert_quiet();
}
h.dispatch(h.ctx(0, &SENDER, who_is(4000))).await;
let relayed = lengths_on(&mut h, 1);
assert_eq!(relayed.len(), 1, "an unknown network is searched for");
h.assert_quiet();
}
#[tokio::test]
async fn r6_a_relay_never_leaves_with_a_hop_count_of_0() {
let mut h = Harness::two_port();
h.learned(3000, 1, &[9]);
let mut to_3000 = npdu_of(20, 3000, &[5]);
to_3000.hop_count = 1;
h.dispatch(h.ctx(0, &SENDER, to_3000)).await;
h.assert_quiet();
let counters = h.counters();
assert_eq!((counters.hop_count_drops, counters.rejects), (1, 0));
let mut global = npdu_of(20, 0xFFFF, &[]);
global.hop_count = 1;
h.dispatch(h.ctx(0, &SENDER, global)).await;
h.assert_quiet();
assert!(h.apdus.try_recv().is_ok(), "delivered locally");
assert_eq!(h.counters().hop_count_drops, 2);
let mut to_2000 = npdu_of(20, 2000, &[5]);
to_2000.hop_count = 1;
h.dispatch(h.ctx(0, &SENDER, to_2000)).await;
assert_eq!(lengths_on(&mut h, 1), [24]);
h.assert_quiet();
}
#[tokio::test]
async fn r7_an_apdu_for_the_routers_address_on_another_port_is_local() {
let mut h = Harness::two_port();
let request = Npdu {
destination: Some(address(2000, &[super::envelope_harness::PORT_MACS[1]])),
hop_count: 255,
expecting_reply: true,
payload: Bytes::from_static(&[0x00, 0x05, 0x01, 0x0C]),
..Npdu::default()
};
h.dispatch(h.ctx(0, &SENDER, request)).await;
h.assert_quiet();
let delivered = h.apdus.try_recv().expect("delivered locally");
assert_eq!(delivered.apdu.as_ref(), [0x00, 0x05, 0x01, 0x0C]);
assert_eq!(delivered.ingress_network, Some(1000));
assert_eq!(delivered.source_mac.as_slice(), SENDER);
assert!(delivered.source_network.is_none());
assert!(!delivered.is_group && !delivered.global_broadcast);
}
#[tokio::test]
async fn r7_the_routers_mac_on_a_remote_network_is_routed_on() {
use super::envelope_harness::PORT_MACS;
let mut h = Harness::two_port();
h.learned(3000, 1, &[9]);
for mac in PORT_MACS {
h.dispatch(h.ctx(0, &SENDER, npdu_of(20, 3000, &[mac])))
.await;
let sent = h.drain(1);
let [SendRequest::Unicast { npdu, mac: to, .. }] = &sent[..] else {
panic!("expected the request routed on port 1, got {sent:?}");
};
assert_eq!(to.as_slice(), [9]);
let npdu = decode_npdu(npdu.clone()).unwrap();
assert_eq!(npdu.destination, Some(address(3000, &[mac])));
h.assert_quiet();
assert!(h.apdus.try_recv().is_err(), "not delivered here");
}
}