use super::super::*;
use super::{make_dest_device, make_time};
use bacnet_types::constructed::{BACnetAddress, BACnetRecipient};
use bacnet_types::enums::ErrorCode;
use bacnet_types::MacAddr;
#[test]
fn object_type_is_notification_class() {
let nc = NotificationClass::new(1, "NC-1").unwrap();
assert_eq!(
nc.object_identifier().object_type(),
ObjectType::NOTIFICATION_CLASS
);
assert_eq!(nc.object_identifier().instance_number(), 1);
}
#[test]
fn read_notification_class_number() {
let nc = NotificationClass::new(42, "NC-42").unwrap();
let val = nc
.read_property(PropertyIdentifier::NOTIFICATION_CLASS, None)
.unwrap();
if let PropertyValue::Unsigned(n) = val {
assert_eq!(n, 42);
} else {
panic!("Expected Unsigned");
}
}
#[test]
fn read_priority_array_index() {
let nc = NotificationClass::new(1, "NC-1").unwrap();
let len = nc
.read_property(PropertyIdentifier::PRIORITY, Some(0))
.unwrap();
if let PropertyValue::Unsigned(n) = len {
assert_eq!(n, 3);
} else {
panic!("Expected Unsigned");
}
let p1 = nc
.read_property(PropertyIdentifier::PRIORITY, Some(1))
.unwrap();
if let PropertyValue::Unsigned(n) = p1 {
assert_eq!(n, 255);
} else {
panic!("Expected Unsigned");
}
}
#[test]
fn read_priority_all() {
let nc = NotificationClass::new(1, "NC-1").unwrap();
let val = nc
.read_property(PropertyIdentifier::PRIORITY, None)
.unwrap();
if let PropertyValue::List(items) = val {
assert_eq!(items.len(), 3);
assert_eq!(items[0], PropertyValue::Unsigned(255));
assert_eq!(items[1], PropertyValue::Unsigned(255));
assert_eq!(items[2], PropertyValue::Unsigned(255));
} else {
panic!("Expected List");
}
}
#[test]
fn read_priority_invalid_index() {
let nc = NotificationClass::new(1, "NC-1").unwrap();
let result = nc.read_property(PropertyIdentifier::PRIORITY, Some(4));
assert!(result.is_err());
}
#[test]
fn read_object_name() {
let nc = NotificationClass::new(1, "NC-1").unwrap();
let val = nc
.read_property(PropertyIdentifier::OBJECT_NAME, None)
.unwrap();
if let PropertyValue::CharacterString(s) = val {
assert_eq!(s, "NC-1");
} else {
panic!("Expected CharacterString");
}
}
#[test]
fn write_notification_class_number() {
let mut nc = NotificationClass::new(1, "NC-1").unwrap();
nc.write_property(
PropertyIdentifier::NOTIFICATION_CLASS,
None,
PropertyValue::Unsigned(99),
None,
)
.unwrap();
assert_eq!(nc.notification_class, 99);
}
#[test]
fn write_notification_class_wrong_type() {
let mut nc = NotificationClass::new(1, "NC-1").unwrap();
let result = nc.write_property(
PropertyIdentifier::NOTIFICATION_CLASS,
None,
PropertyValue::Real(1.0),
None,
);
assert!(result.is_err());
}
#[test]
fn property_list_contains_recipient_list() {
let nc = NotificationClass::new(1, "NC-1").unwrap();
let props = nc.property_list();
assert!(props.contains(&PropertyIdentifier::NOTIFICATION_CLASS));
assert!(props.contains(&PropertyIdentifier::PRIORITY));
assert!(props.contains(&PropertyIdentifier::ACK_REQUIRED));
assert!(props.contains(&PropertyIdentifier::RECIPIENT_LIST));
}
#[test]
fn read_ack_required_default() {
let nc = NotificationClass::new(1, "NC-1").unwrap();
let val = nc
.read_property(PropertyIdentifier::ACK_REQUIRED, None)
.unwrap();
if let PropertyValue::BitString { unused_bits, data } = val {
assert_eq!(unused_bits, 5);
assert_eq!(data, vec![0]); } else {
panic!("Expected BitString");
}
}
#[test]
fn read_ack_required_msb_first() {
let mut nc = NotificationClass::new(1, "NC-1").unwrap();
nc.ack_required = EventTransitionBits::TO_OFFNORMAL | EventTransitionBits::TO_FAULT;
assert_eq!(
nc.read_property(PropertyIdentifier::ACK_REQUIRED, None)
.unwrap(),
PropertyValue::BitString {
unused_bits: 5,
data: vec![0xC0],
}
);
}
#[test]
fn read_recipient_list_empty() {
let nc = NotificationClass::new(1, "NC-1").unwrap();
let val = nc
.read_property(PropertyIdentifier::RECIPIENT_LIST, None)
.unwrap();
assert_eq!(val, PropertyValue::ApplicationData(Vec::new()));
}
#[test]
fn add_destination_device_and_read_back() {
let mut nc = NotificationClass::new(1, "NC-1").unwrap();
nc.add_destination(make_dest_device(99)).unwrap();
let val = nc
.read_property(PropertyIdentifier::RECIPIENT_LIST, None)
.unwrap();
assert_eq!(
val,
PropertyValue::ApplicationData(vec![
0x82, 0x01, 0xFE, 0xB4, 0x00, 0x00, 0x00, 0x00, 0xB4, 0x17, 0x3B, 0x00, 0x00, 0x0C, 0x02, 0x00, 0x00, 0x63, 0x21, 0x01, 0x11, 0x82, 0x05, 0xE0, ])
);
let PropertyValue::ApplicationData(bytes) = &val else {
unreachable!();
};
let decoded = bacnet_encoding::constructed::decode_destination_list(bytes).unwrap();
let dev_oid = ObjectIdentifier::new(ObjectType::DEVICE, 99).unwrap();
assert_eq!(decoded.len(), 1);
assert_eq!(decoded[0].valid_days, DaysOfWeek::all());
assert_eq!(decoded[0].from_time, make_time(0, 0));
assert_eq!(decoded[0].to_time, make_time(23, 59));
assert_eq!(decoded[0].recipient, BACnetRecipient::Device(dev_oid));
assert_eq!(decoded[0].process_identifier, 1);
assert!(decoded[0].issue_confirmed_notifications);
assert_eq!(decoded[0].transitions, EventTransitionBits::all());
}
#[test]
fn add_destination_address_variant() {
let mut nc = NotificationClass::new(1, "NC-1").unwrap();
let mac = MacAddr::from_slice(&[192u8, 168, 1, 100, 0xBA, 0xC0]);
let dest = BACnetDestination {
valid_days: DaysOfWeek::TUESDAY
| DaysOfWeek::WEDNESDAY
| DaysOfWeek::THURSDAY
| DaysOfWeek::FRIDAY
| DaysOfWeek::SATURDAY,
from_time: make_time(8, 0),
to_time: make_time(17, 0),
recipient: BACnetRecipient::Address(BACnetAddress {
network_number: 0,
mac_address: mac.clone(),
}),
process_identifier: 42,
issue_confirmed_notifications: false,
transitions: EventTransitionBits::TO_OFFNORMAL,
};
nc.add_destination(dest.clone()).unwrap();
let val = nc
.read_property(PropertyIdentifier::RECIPIENT_LIST, None)
.unwrap();
assert_eq!(
val,
PropertyValue::ApplicationData(vec![
0x82, 0x01, 0x7C, 0xB4, 0x08, 0x00, 0x00, 0x00, 0xB4, 0x11, 0x00, 0x00, 0x00, 0x1E, 0x21, 0x00, 0x65, 0x06, 0xC0, 0xA8, 0x01, 0x64, 0xBA, 0xC0,
0x1F, 0x21, 0x2A, 0x10, 0x82, 0x05, 0x80, ])
);
let PropertyValue::ApplicationData(bytes) = &val else {
unreachable!();
};
let decoded = bacnet_encoding::constructed::decode_destination_list(bytes).unwrap();
assert_eq!(decoded, vec![dest]);
}
#[test]
fn add_multiple_destinations() {
let mut nc = NotificationClass::new(5, "NC-5").unwrap();
nc.add_destination(make_dest_device(100)).unwrap();
nc.add_destination(make_dest_device(200)).unwrap();
nc.add_destination(make_dest_device(300)).unwrap();
let val = nc
.read_property(PropertyIdentifier::RECIPIENT_LIST, None)
.unwrap();
let PropertyValue::ApplicationData(bytes) = &val else {
panic!("Expected ApplicationData");
};
let decoded = bacnet_encoding::constructed::decode_destination_list(bytes).unwrap();
assert_eq!(decoded.len(), 3);
let instances: Vec<u32> = decoded
.iter()
.map(|d| match &d.recipient {
BACnetRecipient::Device(oid) => oid.instance_number(),
other => panic!("expected Device recipient, got {other:?}"),
})
.collect();
assert_eq!(instances, vec![100, 200, 300]);
}
#[test]
fn write_recipient_list_clears_existing() {
let mut nc = NotificationClass::new(1, "NC-1").unwrap();
nc.add_destination(make_dest_device(10)).unwrap();
nc.add_destination(make_dest_device(20)).unwrap();
assert_eq!(nc.recipient_list.len(), 2);
nc.write_property(
PropertyIdentifier::RECIPIENT_LIST,
None,
PropertyValue::ApplicationData(Vec::new()),
None,
)
.unwrap();
assert!(nc.recipient_list.is_empty());
}
#[test]
fn write_recipient_list_wrong_type_denied() {
let mut nc = NotificationClass::new(1, "NC-1").unwrap();
let result = nc.write_property(
PropertyIdentifier::RECIPIENT_LIST,
None,
PropertyValue::Unsigned(0),
None,
);
assert!(result.is_err());
}
#[test]
fn write_recipient_list_round_trip() {
let mut nc = NotificationClass::new(1, "NC-1").unwrap();
nc.add_destination(make_dest_device(10)).unwrap();
let encoded = nc
.read_property(PropertyIdentifier::RECIPIENT_LIST, None)
.unwrap();
nc.write_property(PropertyIdentifier::RECIPIENT_LIST, None, encoded, None)
.unwrap();
assert_eq!(nc.recipient_list.len(), 1);
assert_eq!(nc.recipient_list[0].process_identifier, 1);
}
#[test]
fn read_event_state_default() {
let nc = NotificationClass::new(1, "NC-1").unwrap();
let val = nc
.read_property(PropertyIdentifier::EVENT_STATE, None)
.unwrap();
assert_eq!(val, PropertyValue::Enumerated(0)); }
#[test]
fn out_of_service_is_unknown() {
let mut nc = NotificationClass::new(1, "NC-1").unwrap();
for result in [
nc.write_property(
PropertyIdentifier::OUT_OF_SERVICE,
None,
PropertyValue::Boolean(true),
None,
),
nc.read_property(PropertyIdentifier::OUT_OF_SERVICE, None)
.map(|_| ()),
] {
assert!(matches!(
result,
Err(Error::Protocol { code, .. })
if code == ErrorCode::UNKNOWN_PROPERTY.to_raw() as u32
));
}
assert_eq!(
nc.read_property(PropertyIdentifier::STATUS_FLAGS, None)
.unwrap(),
PropertyValue::BitString {
unused_bits: 4,
data: vec![0],
}
);
}
#[test]
fn write_unknown_property_denied() {
let mut nc = NotificationClass::new(1, "NC-1").unwrap();
let result = nc.write_property(
PropertyIdentifier::PRESENT_VALUE,
None,
PropertyValue::Real(1.0),
None,
);
assert!(result.is_err());
}