use bacnet_encoding::apdu::{self, Apdu, ComplexAck, SegmentAck as SegmentAckPdu};
use bacnet_encoding::npdu::decode_npdu;
use bacnet_transport::loopback::LoopbackTransport;
use bacnet_transport::port::TransportPort;
use bacnet_types::enums::{AbortReason, ConfirmedServiceChoice};
use bacnet_types::error::RemoteError;
use bytes::Bytes;
use tokio::time::{Duration, timeout};
use super::BACnetClient;
use super::tests::send_routed_response;
#[tokio::test]
async fn routed_segmented_complex_ack_replies_carry_dnet_dadr() {
let client_mac = vec![0x01];
let router_mac = vec![0x02];
let remote_network = 100;
let remote_mac = vec![0x03];
let (client_transport, mut router_transport) =
LoopbackTransport::pair(client_mac.clone(), router_mac.clone());
let mut router_rx = router_transport.start().await.unwrap();
let mut client = BACnetClient::generic_builder()
.transport(client_transport)
.apdu_timeout_ms(2000)
.build()
.await
.unwrap();
let router_mac_for_request = router_mac.clone();
let remote_mac_for_request = remote_mac.clone();
let request_task = tokio::spawn(async move {
let result = client
.confirmed_request_routed(
&router_mac_for_request,
remote_network,
&remote_mac_for_request,
ConfirmedServiceChoice::READ_PROPERTY,
&[0x01],
)
.await;
client.stop().await.unwrap();
result
});
let received = timeout(Duration::from_secs(2), router_rx.recv())
.await
.expect("router timed out waiting for the request")
.expect("router channel closed");
let npdu = decode_npdu(received.npdu).unwrap();
let Apdu::ConfirmedRequest(req) = apdu::decode_apdu(npdu.payload).unwrap() else {
panic!("expected ConfirmedRequest");
};
let invoke_id = req.invoke_id;
let segments: Vec<Bytes> = vec![
Bytes::from_static(&[0xAA, 0x01]),
Bytes::from_static(&[0xBB, 0x02]),
Bytes::from_static(&[0xCC]),
];
for (i, seg) in segments.iter().enumerate() {
let is_last = i == segments.len() - 1;
let ack = Apdu::ComplexAck(ComplexAck {
segmented: true,
more_follows: !is_last,
invoke_id,
sequence_number: Some(i as u8),
proposed_window_size: Some(1),
service_choice: ConfirmedServiceChoice::READ_PROPERTY,
service_ack: seg.clone(),
});
send_routed_response(
&router_transport,
&client_mac,
remote_network,
&remote_mac,
ack,
)
.await;
let reply = timeout(Duration::from_secs(2), router_rx.recv())
.await
.expect("router timed out waiting for a SegmentAck")
.expect("router channel closed");
let reply_npdu = decode_npdu(reply.npdu).unwrap();
let destination = reply_npdu
.destination
.expect("SegmentAck for a routed peer must carry DNET/DADR");
assert_eq!(destination.network, remote_network);
assert_eq!(&destination.mac_address[..], &remote_mac[..]);
let Apdu::SegmentAck(sa) = apdu::decode_apdu(reply_npdu.payload).unwrap() else {
panic!("expected SegmentAck");
};
assert_eq!(sa.invoke_id, invoke_id);
assert_eq!(sa.sequence_number, i as u8);
assert!(!sa.negative_ack);
assert!(!sa.sent_by_server);
}
let result = request_task.await.unwrap().unwrap();
assert_eq!(result, vec![0xAA, 0x01, 0xBB, 0x02, 0xCC]);
router_transport.stop().await.unwrap();
}
#[tokio::test]
async fn routed_reassembly_abort_carries_dnet_dadr() {
let client_mac = vec![0x01];
let router_mac = vec![0x02];
let remote_network = 100;
let remote_mac = vec![0x03];
let (client_transport, mut router_transport) =
LoopbackTransport::pair(client_mac.clone(), router_mac.clone());
let mut router_rx = router_transport.start().await.unwrap();
let mut client = BACnetClient::generic_builder()
.transport(client_transport)
.apdu_timeout_ms(2000)
.build()
.await
.unwrap();
let router_mac_for_request = router_mac.clone();
let remote_mac_for_request = remote_mac.clone();
let request_task = tokio::spawn(async move {
let result = client
.confirmed_request_routed(
&router_mac_for_request,
remote_network,
&remote_mac_for_request,
ConfirmedServiceChoice::READ_PROPERTY,
&[0x01],
)
.await;
client.stop().await.unwrap();
result
});
let received = timeout(Duration::from_secs(2), router_rx.recv())
.await
.expect("router timed out waiting for the request")
.expect("router channel closed");
let npdu = decode_npdu(received.npdu).unwrap();
let Apdu::ConfirmedRequest(req) = apdu::decode_apdu(npdu.payload).unwrap() else {
panic!("expected ConfirmedRequest");
};
let invoke_id = req.invoke_id;
let segment_zero = Apdu::ComplexAck(ComplexAck {
segmented: true,
more_follows: true,
invoke_id,
sequence_number: Some(0),
proposed_window_size: Some(1),
service_choice: ConfirmedServiceChoice::READ_PROPERTY,
service_ack: Bytes::from_static(&[0xAA]),
});
send_routed_response(
&router_transport,
&client_mac,
remote_network,
&remote_mac,
segment_zero,
)
.await;
let seg_ack = timeout(Duration::from_secs(2), router_rx.recv())
.await
.expect("router timed out waiting for the SegmentAck")
.expect("router channel closed");
let seg_ack_npdu = decode_npdu(seg_ack.npdu).unwrap();
assert!(matches!(
apdu::decode_apdu(seg_ack_npdu.payload).unwrap(),
Apdu::SegmentAck(_)
));
let spurious = Apdu::SegmentAck(SegmentAckPdu {
negative_ack: false,
sent_by_server: true,
invoke_id,
sequence_number: 0,
actual_window_size: 1,
});
send_routed_response(
&router_transport,
&client_mac,
remote_network,
&remote_mac,
spurious,
)
.await;
let reply = timeout(Duration::from_secs(2), router_rx.recv())
.await
.expect("router timed out waiting for the Abort")
.expect("router channel closed");
let reply_npdu = decode_npdu(reply.npdu).unwrap();
let destination = reply_npdu
.destination
.expect("Abort from stored session state must carry DNET/DADR");
assert_eq!(destination.network, remote_network);
assert_eq!(&destination.mac_address[..], &remote_mac[..]);
let Apdu::Abort(abort) = apdu::decode_apdu(reply_npdu.payload).unwrap() else {
panic!("expected Abort");
};
assert_eq!(abort.invoke_id, invoke_id);
assert!(!abort.sent_by_server);
assert_eq!(abort.abort_reason, AbortReason::INVALID_APDU_IN_THIS_STATE);
let result = request_task.await.unwrap();
assert!(
matches!(result, Err(super::Error::Remote(RemoteError::Abort { .. }))),
"the caller must observe the abort, got {result:?}"
);
router_transport.stop().await.unwrap();
}
#[tokio::test]
async fn routed_unsolicited_segmented_ack_abort_carries_dnet_dadr() {
let client_mac = vec![0x01];
let router_mac = vec![0x02];
let remote_network = 100;
let remote_mac = vec![0x03];
let (client_transport, mut router_transport) =
LoopbackTransport::pair(client_mac.clone(), router_mac);
let mut router_rx = router_transport.start().await.unwrap();
let mut client = BACnetClient::generic_builder()
.transport(client_transport)
.apdu_timeout_ms(2000)
.build()
.await
.unwrap();
let unsolicited = Apdu::ComplexAck(ComplexAck {
segmented: true,
more_follows: true,
invoke_id: 42,
sequence_number: Some(0),
proposed_window_size: Some(1),
service_choice: ConfirmedServiceChoice::READ_PROPERTY,
service_ack: Bytes::from_static(&[0xEE]),
});
send_routed_response(
&router_transport,
&client_mac,
remote_network,
&remote_mac,
unsolicited,
)
.await;
let reply = timeout(Duration::from_secs(2), router_rx.recv())
.await
.expect("router timed out waiting for the Abort")
.expect("router channel closed");
let reply_npdu = decode_npdu(reply.npdu).unwrap();
let destination = reply_npdu
.destination
.expect("Abort for a routed peer must carry DNET/DADR");
assert_eq!(destination.network, remote_network);
assert_eq!(&destination.mac_address[..], &remote_mac[..]);
let Apdu::Abort(abort) = apdu::decode_apdu(reply_npdu.payload).unwrap() else {
panic!("expected Abort");
};
assert_eq!(abort.invoke_id, 42);
assert!(!abort.sent_by_server);
assert_eq!(abort.abort_reason, AbortReason::INVALID_APDU_IN_THIS_STATE);
client.stop().await.unwrap();
router_transport.stop().await.unwrap();
}