use super::*;
#[test]
fn server_config_cov_retry_timeout_default() {
let config = ServerConfig::default();
assert_eq!(config.cov_retry_timeout_ms, 3000);
}
#[test]
fn server_config_time_sync_callback_default_is_none() {
let config = ServerConfig::default();
assert!(config.on_time_sync.is_none());
assert!(config.policy.is_none());
}
#[tokio::test]
async fn default_and_clockless_start_bind_truthful_device_clock() {
use bacnet_objects::device::{DeviceConfig, DeviceObject};
use bacnet_objects::traits::BACnetObject;
use bacnet_types::bitstring::ServicesSupported;
use bacnet_types::enums::ServiceSupported;
async fn assert_mode(clockless: bool) {
let device = DeviceObject::new(DeviceConfig::default()).unwrap();
let oid = device.object_identifier();
let mut db = ObjectDatabase::new();
db.add(Box::new(device)).unwrap();
let transport = BipTransport::new(Ipv4Addr::LOCALHOST, 0, Ipv4Addr::BROADCAST);
let mut server = if clockless {
BACnetServer::start_clockless(ServerConfig::default(), db, transport)
.await
.unwrap()
} else {
BACnetServer::start(ServerConfig::default(), db, transport)
.await
.unwrap()
};
let db = server.db.read().await;
let device = db.get(&oid).unwrap();
for property in [
PropertyIdentifier::LOCAL_DATE,
PropertyIdentifier::LOCAL_TIME,
PropertyIdentifier::UTC_OFFSET,
PropertyIdentifier::DAYLIGHT_SAVINGS_STATUS,
] {
assert_eq!(device.read_property(property, None).is_ok(), !clockless);
}
let PropertyValue::BitString { data, .. } = device
.read_property(PropertyIdentifier::PROTOCOL_SERVICES_SUPPORTED, None)
.unwrap()
else {
panic!("expected services bit string");
};
let services = ServicesSupported::from_bacnet(&data);
assert_eq!(
services.contains(ServiceSupported::TIME_SYNCHRONIZATION),
!clockless
);
assert_eq!(
services.contains(ServiceSupported::UTC_TIME_SYNCHRONIZATION),
!clockless
);
drop(db);
let pics = server
.generate_pics(&crate::pics::PicsConfig::default())
.await;
for service_name in ["TimeSynchronization", "UTCTimeSynchronization"] {
let executed = pics
.supported_services
.iter()
.find(|service| service.service_name == service_name)
.is_some_and(|service| service.executor);
assert_eq!(executed, !clockless, "PICS service {service_name}");
}
server.stop().await.unwrap();
}
assert_mode(false).await;
assert_mode(true).await;
}
#[test]
fn all_builders_assign_the_policy() {
let generic = BACnetServer::<BipTransport>::generic_builder()
.policy(crate::FnPolicy::new().life_safety_operation(|_| true));
assert!(generic.config.policy.is_some());
let bip = BACnetServer::<BipTransport>::bip_builder()
.policy(crate::FnPolicy::new().life_safety_operation(|_| true));
assert!(bip.config.policy.is_some());
#[cfg(feature = "sc-tls")]
{
let sc = BACnetServer::sc_builder()
.policy(crate::FnPolicy::new().life_safety_operation(|_| true));
assert!(sc.config.policy.is_some());
}
}
#[test]
fn all_builders_register_the_same_validated_device_binding_surface() {
let identifier = ObjectIdentifier::new(ObjectType::DEVICE, 44).unwrap();
let binding = DeviceBinding::local(identifier, [1, 2, 3]).unwrap();
let generic = BACnetServer::<BipTransport>::generic_builder()
.device_binding(binding.clone())
.unwrap();
assert_eq!(generic.configured_device_bindings.len(), 1);
let bip = BACnetServer::<BipTransport>::bip_builder()
.device_binding(binding.clone())
.unwrap();
assert_eq!(bip.configured_device_bindings.len(), 1);
#[cfg(feature = "sc-tls")]
{
let sc = BACnetServer::sc_builder().device_binding(binding).unwrap();
assert_eq!(sc.configured_device_bindings.len(), 1);
}
}
#[test]
fn builder_rejects_duplicate_configured_device_before_start() {
let identifier = ObjectIdentifier::new(ObjectType::DEVICE, 45).unwrap();
let binding = DeviceBinding::local(identifier, [1, 2, 3]).unwrap();
let builder = BACnetServer::<BipTransport>::generic_builder()
.device_binding(binding.clone())
.unwrap();
assert!(builder.device_binding(binding).is_err());
}
#[cfg(feature = "sc-tls")]
#[tokio::test]
async fn sc_builder_rejects_broadcast_binding_before_tls_prerequisites() {
let identifier = ObjectIdentifier::new(ObjectType::DEVICE, 46).unwrap();
let binding =
DeviceBinding::local(identifier, bacnet_transport::sc_frame::BROADCAST_VMAC).unwrap();
let builder = BACnetServer::sc_builder()
.device_uuid(super::sc_builder::TEST_DEVICE_UUID)
.device_binding(binding)
.unwrap();
let Err(error) = builder.build().await else {
panic!("invalid SC binding unexpectedly started a server");
};
assert!(
error.to_string().contains("broadcast or group address"),
"binding validation must precede TLS prerequisites: {error}"
);
}
#[test]
fn server_config_event_enrollment_defaults() {
let config = ServerConfig::default();
assert!(config.enable_event_enrollment);
assert_eq!(config.event_enrollment_interval_secs, 10);
assert!(!config.enable_fault_detection);
}
#[test]
fn event_enrollment_period_clamps_zero_to_one_second() {
assert_eq!(
super::lifecycle::event_enrollment_period(0),
Duration::from_secs(1)
);
}
#[test]
fn event_enrollment_period_passes_through_nonzero() {
assert_eq!(
super::lifecycle::event_enrollment_period(30),
Duration::from_secs(30)
);
}
#[test]
fn all_builders_assign_segmentation_supported() {
let generic = BACnetServer::<BipTransport>::generic_builder()
.device(bacnet_objects::DeviceIdentity::default().with_segmentation(Segmentation::RECEIVE));
assert_eq!(generic.config.device.segmentation(), Segmentation::RECEIVE);
let bip = BACnetServer::<BipTransport>::bip_builder()
.device(bacnet_objects::DeviceIdentity::default().with_segmentation(Segmentation::BOTH));
assert_eq!(bip.config.device.segmentation(), Segmentation::BOTH);
#[cfg(feature = "sc-tls")]
{
let sc = BACnetServer::sc_builder().device(
bacnet_objects::DeviceIdentity::default().with_segmentation(Segmentation::RECEIVE),
);
assert_eq!(sc.config.device.segmentation(), Segmentation::RECEIVE);
}
}
#[test]
fn all_builders_assign_event_enrollment_config() {
let generic = BACnetServer::<BipTransport>::generic_builder()
.enable_event_enrollment(false)
.event_enrollment_interval_secs(7);
assert!(!generic.config.enable_event_enrollment);
assert_eq!(generic.config.event_enrollment_interval_secs, 7);
assert_eq!(generic.config.cov_retry_timeout_ms, 3000);
assert!(!generic.config.enable_fault_detection);
let bip = BACnetServer::<BipTransport>::bip_builder()
.enable_event_enrollment(false)
.event_enrollment_interval_secs(11);
assert!(!bip.config.enable_event_enrollment);
assert_eq!(bip.config.event_enrollment_interval_secs, 11);
assert_eq!(bip.config.cov_retry_timeout_ms, 3000);
assert!(!bip.config.enable_fault_detection);
#[cfg(feature = "sc-tls")]
{
let sc = BACnetServer::sc_builder()
.enable_event_enrollment(false)
.event_enrollment_interval_secs(13);
assert!(!sc.config.enable_event_enrollment);
assert_eq!(sc.config.event_enrollment_interval_secs, 13);
assert_eq!(sc.config.cov_retry_timeout_ms, 3000);
}
}
#[tokio::test(start_paused = true)]
async fn server_enrollment_task_evaluates_at_startup_on_its_configured_interval() {
use bacnet_objects::analog::AnalogInputObject;
use bacnet_objects::event_enrollment::EventEnrollmentObject;
use bacnet_objects::traits::BACnetObject;
use bacnet_types::constructed::{BACnetDeviceObjectPropertyReference, BACnetEventParameter};
use bacnet_types::enums::EngineeringUnits;
use bacnet_types::enums::{EventState, EventType, PropertyIdentifier};
let mut db = ObjectDatabase::new();
let mut ai = AnalogInputObject::new(1, "AI-1", EngineeringUnits::DEGREES_CELSIUS).unwrap();
ai.set_present_value(150.0); let ai_oid = ai.object_identifier();
db.add(Box::new(ai)).unwrap();
let mut ee = EventEnrollmentObject::new(1, "EE-1", EventType::OUT_OF_RANGE).unwrap();
ee.set_object_property_reference(Some(BACnetDeviceObjectPropertyReference::new_local(
ai_oid,
PropertyIdentifier::PRESENT_VALUE.to_raw(),
)))
.unwrap();
ee.set_event_parameters(BACnetEventParameter::OutOfRange {
time_delay: 0,
low_limit: 0.0,
high_limit: 100.0,
deadband: 1.0,
});
ee.set_event_enable(bacnet_types::bitstring::EventTransitionBits::all());
let ee_oid = ee.object_identifier();
db.add(Box::new(ee)).unwrap();
let config = ServerConfig {
event_enrollment_interval_secs: 3600,
..ServerConfig::default()
};
let transport = BipTransport::new(Ipv4Addr::LOCALHOST, 0, Ipv4Addr::BROADCAST);
let mut server = BACnetServer::start(config, db, transport)
.await
.expect("server should start");
tokio::time::sleep(Duration::from_millis(50)).await;
{
let guard = server.db.read().await;
let state = guard
.get(&ee_oid)
.unwrap()
.read_property(PropertyIdentifier::EVENT_STATE, None)
.unwrap();
assert_eq!(
state,
PropertyValue::Enumerated(EventState::HIGH_LIMIT.to_raw())
);
}
{
let mut guard = server.db.write().await;
guard
.get_mut(&ee_oid)
.unwrap()
.set_event_state_internal(EventState::NORMAL)
.unwrap();
}
tokio::time::sleep(Duration::from_secs(60)).await;
{
let guard = server.db.read().await;
let state = guard
.get(&ee_oid)
.unwrap()
.read_property(PropertyIdentifier::EVENT_STATE, None)
.unwrap();
assert_eq!(
state,
PropertyValue::Enumerated(EventState::NORMAL.to_raw()),
"a pass ran within 60s, so the configured interval was not honored"
);
}
server.stop().await.expect("server should stop");
}
#[tokio::test]
async fn server_spawns_enrollment_task_without_fault_detection() {
let config = ServerConfig {
enable_fault_detection: false,
enable_event_enrollment: true,
..ServerConfig::default()
};
let transport = BipTransport::new(Ipv4Addr::LOCALHOST, 0, Ipv4Addr::BROADCAST);
let mut server = BACnetServer::start(config, ObjectDatabase::new(), transport)
.await
.expect("server should start");
assert!(server.event_enrollment_task.is_some());
assert!(server.fault_detection_task.is_none());
server.stop().await.expect("server should stop");
}
#[tokio::test]
async fn server_runs_fault_detection_without_enrollment() {
let config = ServerConfig {
enable_fault_detection: true,
enable_event_enrollment: false,
..ServerConfig::default()
};
let transport = BipTransport::new(Ipv4Addr::LOCALHOST, 0, Ipv4Addr::BROADCAST);
let mut server = BACnetServer::start(config, ObjectDatabase::new(), transport)
.await
.expect("server should start");
assert!(server.fault_detection_task.is_some());
assert!(server.event_enrollment_task.is_none());
server.stop().await.expect("server should stop");
}
#[test]
fn identity_rejects_below_minimum_local_apdu_capacity() {
assert!(
bacnet_objects::DeviceIdentity::default()
.with_max_apdu(49)
.is_err()
);
}
fn test_mac(byte: u8) -> MacAddr {
MacAddr::from_slice(&[127, 0, 0, byte, 0xBA, 0xC0])
}
#[test]
fn cov_ack_result_debug_and_eq() {
assert_eq!(CovAckResult::Ack, CovAckResult::Ack);
assert_ne!(
CovAckResult::Ack,
CovAckResult::Error(RemoteError::Abort {
reason: AbortReason::OTHER
})
);
let _debug = format!("{:?}", CovAckResult::Ack);
}
#[test]
fn default_apdu_retries_constant() {
assert_eq!(DEFAULT_APDU_RETRIES, 3);
}
#[test]
fn seg_receiver_timeout_is_four_times_configured_tseg() {
let timeout =
super::segmented_receive::validate_segment_timeout(&ServerConfig::default()).unwrap();
assert_eq!(timeout, Duration::from_secs(20));
}
#[test]
fn segmented_transaction_key_identity_matrix() {
let router_a = test_mac(1);
let router_b = test_mac(2);
let remote_a = NpduAddress {
network: 100,
mac_address: MacAddr::from_slice(&[0x0A]),
};
let remote_b = NpduAddress {
network: 100,
mac_address: MacAddr::from_slice(&[0x0B]),
};
let other_network = NpduAddress {
network: 101,
mac_address: remote_a.mac_address.clone(),
};
let routed_a = segmented_transaction_key(
&router_a,
Some(&remote_a),
7,
bacnet_transport::port::TransportProvenance::unverified(),
);
assert_eq!(
routed_a,
segmented_transaction_key(
&router_b,
Some(&remote_a),
7,
bacnet_transport::port::TransportProvenance::unverified()
)
);
assert_eq!(routed_a.0, MacAddr::new());
assert_ne!(
routed_a,
segmented_transaction_key(
&router_a,
Some(&remote_b),
7,
bacnet_transport::port::TransportProvenance::unverified()
)
);
assert_ne!(
routed_a,
segmented_transaction_key(
&router_a,
Some(&other_network),
7,
bacnet_transport::port::TransportProvenance::unverified()
)
);
assert_ne!(
routed_a,
segmented_transaction_key(
&router_a,
Some(&remote_a),
8,
bacnet_transport::port::TransportProvenance::unverified()
)
);
let local_a = segmented_transaction_key(
&router_a,
None,
7,
bacnet_transport::port::TransportProvenance::unverified(),
);
let local_b = segmented_transaction_key(
&router_b,
None,
7,
bacnet_transport::port::TransportProvenance::unverified(),
);
assert_ne!(local_a, local_b);
assert_ne!(local_a, routed_a);
assert_ne!(
local_a,
segmented_transaction_key(
&router_a,
None,
8,
bacnet_transport::port::TransportProvenance::unverified()
)
);
for invalid in [
NpduAddress {
network: 0,
mac_address: MacAddr::from_slice(&[0x0A]),
},
NpduAddress {
network: 0xFFFF,
mac_address: MacAddr::from_slice(&[0x0A]),
},
NpduAddress {
network: 100,
mac_address: MacAddr::new(),
},
] {
let key_a = segmented_transaction_key(
&router_a,
Some(&invalid),
7,
bacnet_transport::port::TransportProvenance::unverified(),
);
let key_b = segmented_transaction_key(
&router_b,
Some(&invalid),
7,
bacnet_transport::port::TransportProvenance::unverified(),
);
assert_ne!(key_a, key_b);
assert_eq!(key_a.0, router_a);
assert_eq!(key_a.1.as_ref(), Some(&invalid));
}
}
#[test]
fn service_reject_error_preserves_reason_on_wire_response() {
let response = BACnetServer::<BipTransport>::error_apdu_from_error(
0x31,
ConfirmedServiceChoice::GET_ENROLLMENT_SUMMARY,
&Error::reject(RejectReason::UNDEFINED_ENUMERATION),
);
assert_eq!(
response,
Apdu::Reject(RejectPdu {
invoke_id: 0x31,
reject_reason: RejectReason::UNDEFINED_ENUMERATION,
})
);
}
#[tokio::test]
async fn reply_tx_response_preserves_routed_npdu_destination() {
use bacnet_encoding::apdu::decode_apdu;
use bacnet_encoding::npdu::decode_npdu;
let network = Arc::new(NetworkLayer::new(BipTransport::new(
Ipv4Addr::LOCALHOST,
0,
Ipv4Addr::BROADCAST,
)));
let confirmed_request_tracker = Arc::new(ConfirmedRequestTracker::default());
let config = ServerConfig::default();
let source_mac = test_mac(1);
let routed_source = NpduAddress {
network: 100,
mac_address: MacAddr::from_slice(&[0x0A, 0x0B]),
};
let req = ConfirmedRequestPdu {
segmented: false,
more_follows: false,
segmented_response_accepted: false,
max_segments: None,
max_apdu_length: 480,
invoke_id: 0x31,
sequence_number: None,
proposed_window_size: None,
service_choice: ConfirmedServiceChoice::VT_OPEN,
service_request: Bytes::new(),
};
let (tx, rx) = oneshot::channel();
BACnetServer::<BipTransport>::handle_confirmed_request(
&RequestServices {
..RequestServices::for_test(Arc::clone(&network), config.clone())
},
&confirmed_request_tracker,
&Arc::new(crate::server::request_tasks::RequestTasks::default()).spawner(),
&source_mac,
Some(routed_source.clone()),
req,
Some(tx),
)
.await;
let npdu = decode_npdu(rx.await.expect("reply_tx should receive response")).unwrap();
assert_eq!(npdu.destination, Some(routed_source));
match decode_apdu(npdu.payload).unwrap() {
Apdu::Reject(reject) => {
assert_eq!(reject.invoke_id, 0x31);
assert_eq!(reject.reject_reason, RejectReason::UNRECOGNIZED_SERVICE);
}
other => panic!("expected Reject, got {other:?}"),
}
}
#[test]
fn max_neg_segment_ack_retries_constant() {
assert_eq!(MAX_NEG_SEGMENT_ACK_RETRIES, 3);
}
#[tokio::test]
async fn bip_builder_shares_the_port_by_address_only_with_an_address() {
let built = BACnetServer::<BipTransport>::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);
}