use super::*;
use bacnet_transport::bbmd::ForeignDevicePolicy;
use bacnet_transport::bip::{BbmdOptions, ForeignDeviceConfig, ForeignRegistrationOutcome};
use bacnet_transport::bvll::BvlcResultCode;
use bacnet_transport::loopback::LoopbackTransport;
use std::net::Ipv4Addr;
#[tokio::test]
async fn bip_builder_foreign_device_starts_without_waiting_for_bbmd_acceptance() {
let peer = tokio::net::UdpSocket::bind((Ipv4Addr::LOCALHOST, 0))
.await
.unwrap();
let mut client = timeout(
Duration::from_secs(1),
BACnetClient::bip_builder()
.interface(Ipv4Addr::LOCALHOST)
.port(0)
.mode(BipMode::Foreign(ForeignDeviceConfig {
bbmd_ip: Ipv4Addr::LOCALHOST,
bbmd_port: peer.local_addr().unwrap().port(),
ttl: 60,
renewal_interval: Some(Duration::from_secs(20)),
}))
.build(),
)
.await
.expect("builder must not wait for a BBMD result")
.unwrap();
let mut packet = [0; 64];
let (len, _) = peer.recv_from(&mut packet).await.unwrap();
assert_eq!(&packet[..len], &[0x81, 5, 0, 6, 0, 60]);
let snapshot = client.transport().bvlc_client_snapshot();
assert!(snapshot.running);
assert_eq!(snapshot.foreign_registration.accepted, 0);
assert_eq!(snapshot.foreign_registration.last_ttl, Some(60));
client.stop().await.unwrap();
assert!(!client.transport().bvlc_client_snapshot().running);
}
#[tokio::test]
async fn bip_builder_foreign_device_validates_settings_before_io() {
for (ttl, interval) in [
(0, None),
(60, Some(Duration::ZERO)),
(60, Some(Duration::from_secs(60))),
] {
let result = BACnetClient::bip_builder()
.interface(Ipv4Addr::new(192, 0, 2, 1))
.port(0)
.mode(BipMode::Foreign(ForeignDeviceConfig {
bbmd_ip: Ipv4Addr::LOCALHOST,
bbmd_port: 47808,
ttl,
renewal_interval: interval,
}))
.build()
.await;
match result {
Err(Error::Encoding(message)) => assert!(message.contains("renewal interval")),
Err(other) => panic!("expected local configuration error, got {other}"),
Ok(mut client) => {
client.stop().await.unwrap();
panic!("invalid foreign config accepted");
}
}
}
}
async fn foreign_client_answered_with(
code: BvlcResultCode,
renewal_interval: Option<Duration>,
) -> BACnetClient<BipTransport> {
let bbmd = tokio::net::UdpSocket::bind((Ipv4Addr::LOCALHOST, 0))
.await
.unwrap();
let client = BACnetClient::bip_builder()
.interface(Ipv4Addr::LOCALHOST)
.port(0)
.mode(BipMode::Foreign(ForeignDeviceConfig {
bbmd_ip: Ipv4Addr::LOCALHOST,
bbmd_port: bbmd.local_addr().unwrap().port(),
ttl: 60,
renewal_interval,
}))
.build()
.await
.unwrap();
let [high, low] = code.to_raw().to_be_bytes();
tokio::spawn(async move {
let mut packet = [0; 64];
while let Ok((len, from)) = bbmd.recv_from(&mut packet).await {
if packet[..len] == [0x81, 5, 0, 6, 0, 60] {
let _ = bbmd.send_to(&[0x81, 0, 0, 6, high, low], from).await;
}
}
});
timeout(Duration::from_secs(20), async {
while client.foreign_registration().unwrap().last_result != Some(code) {
tokio::time::sleep(Duration::from_millis(1)).await;
}
})
.await
.expect("the client saw the BBMD's answer");
client
}
#[tokio::test]
async fn bip_builder_foreign_mode_reports_the_lease_and_renews_on_its_interval() {
let mut client = foreign_client_answered_with(
BvlcResultCode::SUCCESSFUL_COMPLETION,
Some(Duration::from_secs(10)),
)
.await;
let registration = client.foreign_registration().unwrap();
assert!(registration.registered);
assert_eq!(
registration.last_outcome,
Some(ForeignRegistrationOutcome::Accepted)
);
assert!(registration.lease_remaining.unwrap() > Duration::from_secs(50));
assert!(registration.next_attempt_in.unwrap() <= Duration::from_secs(10));
client.stop().await.unwrap();
}
#[tokio::test]
async fn bip_builder_foreign_mode_reports_a_refused_registration() {
let mut client =
foreign_client_answered_with(BvlcResultCode::REGISTER_FOREIGN_DEVICE_NAK, None).await;
let registration = client.foreign_registration().unwrap();
assert!(!registration.registered);
assert_eq!(registration.refused, 1);
assert_eq!(
registration.last_result,
Some(BvlcResultCode::REGISTER_FOREIGN_DEVICE_NAK)
);
assert_eq!(
registration.last_outcome,
Some(ForeignRegistrationOutcome::Refused)
);
client.stop().await.unwrap();
}
#[tokio::test]
async fn bip_builder_bbmd_mode_starts_a_bbmd_with_its_options() {
let policy = ForeignDevicePolicy {
max_ttl: 120,
..ForeignDevicePolicy::default()
};
let mut client = BACnetClient::bip_builder()
.interface(Ipv4Addr::LOCALHOST)
.port(0)
.broadcast_address(Ipv4Addr::LOCALHOST)
.mode(BipMode::Bbmd(BbmdOptions {
foreign_device_policy: Some(policy.clone()),
management_acl: vec![[127, 0, 0, 9]],
..BbmdOptions::default()
}))
.build()
.await
.unwrap();
{
let state = client.transport().bbmd_state().unwrap().lock().unwrap();
assert_eq!(state.foreign_device_policy(), Some(&policy));
assert!(state.is_management_allowed(&[127, 0, 0, 9]));
assert!(!state.is_management_allowed(&[127, 0, 0, 1]));
}
let registration = client.foreign_registration().unwrap();
assert_eq!((registration.attempts, registration.registered), (0, false));
client.stop().await.unwrap();
}
fn assert_apdu_tuning(config: &ClientConfig) {
assert_eq!(config.apdu_retries, 7);
assert_eq!(config.max_segments, Some(8));
assert!(!config.segmented_response_accepted);
assert_eq!(config.proposed_window_size, 4);
}
#[tokio::test]
async fn generic_builder_sets_apdu_tuning_options() {
let (transport, _peer_transport) = LoopbackTransport::pair(vec![0x01], vec![0x02]);
let mut client = BACnetClient::generic_builder()
.transport(transport)
.apdu_retries(7)
.max_segments(Some(8))
.segmented_response_accepted(false)
.proposed_window_size(4)
.build()
.await
.unwrap();
assert_apdu_tuning(&client.config);
client.stop().await.unwrap();
}
#[tokio::test]
async fn bip_builder_sets_apdu_tuning_options() {
let mut client = BACnetClient::bip_builder()
.interface(Ipv4Addr::LOCALHOST)
.port(0)
.apdu_retries(7)
.max_segments(Some(8))
.segmented_response_accepted(false)
.proposed_window_size(4)
.build()
.await
.unwrap();
assert_apdu_tuning(&client.config);
client.stop().await.unwrap();
}
#[tokio::test]
async fn builder_rejects_invalid_proposed_window_size() {
let (transport, _peer_transport) = LoopbackTransport::pair(vec![0x10], vec![0x11]);
let result = BACnetClient::generic_builder()
.transport(transport)
.proposed_window_size(0)
.build()
.await;
assert!(result.is_err());
}
#[tokio::test]
async fn builder_rejects_max_segments_below_protocol_minimum() {
for invalid in [0, 1] {
let (transport, _peer_transport) = LoopbackTransport::pair(vec![0x10], vec![0x11]);
let result = BACnetClient::generic_builder()
.transport(transport)
.max_segments(Some(invalid))
.build()
.await;
match result {
Err(Error::Range(refused)) => {
assert_eq!(
refused.to_string(),
format!(
"value out of range: invalid max-segments-accepted {invalid}; \
expected 2..=255 or unspecified"
)
);
}
Err(other) => panic!("expected a range error, got {other}"),
Ok(mut client) => {
client.stop().await.unwrap();
panic!("max_segments={invalid} must be rejected");
}
}
}
}
#[tokio::test]
async fn bip_builder_shares_the_port_by_address_only_with_an_address() {
let built = BACnetClient::bip_builder()
.interface(Ipv4Addr::UNSPECIFIED)
.port(0xBAC0)
.share_port_by_address(true)
.build()
.await;
let Err(Error::Transport(refused)) = built else {
panic!("build must refuse");
};
assert_eq!(refused.kind(), std::io::ErrorKind::InvalidInput);
}
#[tokio::test]
async fn bip_builder_joins_a_multicast_group_only_with_an_interface() {
let group_port = std::net::UdpSocket::bind((Ipv4Addr::UNSPECIFIED, 0))
.unwrap()
.local_addr()
.unwrap()
.port();
let group = bacnet_transport::bip::BipMulticast::new(Ipv4Addr::new(239, 255, 96, 1))
.unwrap()
.with_port(group_port)
.unwrap();
let Err(refused) = BACnetClient::bip_builder()
.port(0)
.multicast(Some(group))
.build()
.await
else {
panic!("build must refuse a wildcard interface");
};
assert!(
refused.to_string().contains("explicit interface"),
"{refused}"
);
let mut client = BACnetClient::bip_builder()
.interface(Ipv4Addr::LOCALHOST)
.port(group_port)
.multicast(Some(group))
.build()
.await
.unwrap();
assert_eq!(client.transport().multicast(), Some(group));
client.stop().await.unwrap();
}