use std::time::Instant;
use bacnet_encoding::apdu::{self, Apdu, SimpleAck, encode_apdu};
use bacnet_network::layer::NetworkLayer;
use bacnet_npdu::NetworkPriority;
use bacnet_transport::loopback::LoopbackTransport;
use bacnet_types::MacAddr;
use bacnet_types::enums::{ConfirmedServiceChoice, ObjectType, Segmentation};
use bacnet_types::primitives::ObjectIdentifier;
use bytes::BytesMut;
use tokio::time::{Duration, timeout};
use crate::discovery::DiscoveredDevice;
use super::BACnetClient;
async fn client_with_peer_advertising(
max_apdu_length: u32,
) -> (
BACnetClient<LoopbackTransport>,
Vec<u8>,
NetworkLayer<LoopbackTransport>,
bacnet_network::layer::ApduReceiver,
) {
let client_mac = vec![0x01];
let remote_mac = vec![0x02];
let (client_transport, remote_transport) =
LoopbackTransport::pair(client_mac, remote_mac.clone());
let mut remote_network = NetworkLayer::new(remote_transport);
let remote_rx = remote_network.start().await.unwrap();
let client = BACnetClient::generic_builder()
.transport(client_transport)
.build()
.await
.unwrap();
client.device_table.lock().await.upsert(DiscoveredDevice {
object_identifier: ObjectIdentifier::new(ObjectType::DEVICE, 2003).unwrap(),
mac_address: MacAddr::from_slice(&remote_mac),
max_apdu_length,
segmentation_supported: Segmentation::NONE,
max_segments_accepted: None,
vendor_id: 42,
last_seen: Instant::now(),
source_network: None,
source_address: None,
});
(client, remote_mac, remote_network, remote_rx)
}
#[tokio::test]
async fn peer_max_apdu_too_small_for_an_unsegmented_apdu_sends_nothing() {
for advertised in [0u32, 1, 2, 3] {
let (mut client, remote_mac, mut remote_network, mut remote_rx) =
client_with_peer_advertising(advertised).await;
let result = client
.confirmed_request(&remote_mac, ConfirmedServiceChoice::READ_PROPERTY, &[])
.await;
assert!(
timeout(Duration::from_millis(50), remote_rx.recv())
.await
.is_err(),
"advertised max APDU {advertised} must produce no outbound frame"
);
let err = result.expect_err(&format!(
"advertised max APDU {advertised} is below MinimumMessageSize and must be refused"
));
assert!(
err.to_string().contains("MinimumMessageSize"),
"error should name MinimumMessageSize, got: {err}"
);
remote_network.stop().await.unwrap();
client.stop().await.unwrap();
}
}
#[tokio::test]
async fn peer_max_apdu_below_the_floor_but_above_the_apdu_size_is_also_refused() {
for advertised in [4u32, 6, 49] {
let (mut client, remote_mac, mut remote_network, mut remote_rx) =
client_with_peer_advertising(advertised).await;
let result = client
.confirmed_request(&remote_mac, ConfirmedServiceChoice::READ_PROPERTY, &[])
.await;
assert!(
timeout(Duration::from_millis(50), remote_rx.recv())
.await
.is_err(),
"advertised max APDU {advertised} must produce no outbound frame"
);
assert!(
result.is_err(),
"advertised max APDU {advertised} is below the 50-octet floor"
);
remote_network.stop().await.unwrap();
client.stop().await.unwrap();
}
}
#[tokio::test]
async fn nonempty_request_to_peer_below_minimum_message_size_sends_nothing() {
let (mut client, remote_mac, mut remote_network, mut remote_rx) =
client_with_peer_advertising(3).await;
let result = client
.confirmed_request(
&remote_mac,
ConfirmedServiceChoice::READ_PROPERTY,
&[0x01, 0x02, 0x03],
)
.await;
assert!(
result.is_err(),
"a non-conformant limit refuses any payload"
);
assert!(
timeout(Duration::from_millis(50), remote_rx.recv())
.await
.is_err(),
"no outbound frame"
);
remote_network.stop().await.unwrap();
client.stop().await.unwrap();
}
#[tokio::test]
async fn confirmed_request_at_minimum_message_size_is_sent_unsegmented() {
let (mut client, remote_mac, mut remote_network, mut remote_rx) =
client_with_peer_advertising(50).await;
let responder = tokio::spawn(async move {
let received = timeout(Duration::from_secs(1), remote_rx.recv())
.await
.expect("remote timed out")
.expect("remote channel closed");
let Apdu::ConfirmedRequest(req) = apdu::decode_apdu(received.apdu).unwrap() else {
panic!("expected ConfirmedRequest");
};
assert!(
!req.segmented,
"an empty request fits MinimumMessageSize unsegmented"
);
let ack = Apdu::SimpleAck(SimpleAck {
invoke_id: req.invoke_id,
service_choice: req.service_choice,
});
let mut buf = BytesMut::new();
encode_apdu(&mut buf, &ack).unwrap();
remote_network
.send_apdu(&buf, &received.source_mac, false, NetworkPriority::Normal)
.await
.unwrap();
remote_network.stop().await.unwrap();
});
let result = client
.confirmed_request(&remote_mac, ConfirmedServiceChoice::READ_PROPERTY, &[])
.await;
assert!(
result.expect("50 octets is a conformant limit").is_empty(),
"SimpleAck carries no service data"
);
responder.await.unwrap();
client.stop().await.unwrap();
}