use super::*;
use bacnet_objects::notification_class::NotificationClass;
use bacnet_services::list_manipulation::ListElementRequest;
use bacnet_types::bitstring::{DaysOfWeek, EventTransitionBits};
use bacnet_types::constructed::{BACnetDestination, BACnetRecipient};
use bacnet_types::primitives::Time;
fn destination(device_instance: u32) -> BACnetDestination {
let t = |hour, minute| Time {
hour,
minute,
second: 0,
hundredths: 0,
};
BACnetDestination {
valid_days: DaysOfWeek::all(),
from_time: t(0, 0),
to_time: t(23, 59),
recipient: BACnetRecipient::Device(
ObjectIdentifier::new(ObjectType::DEVICE, device_instance).unwrap(),
),
process_identifier: device_instance,
issue_confirmed_notifications: false,
transitions: EventTransitionBits::all(),
}
}
fn framed(destinations: &[BACnetDestination]) -> Vec<u8> {
let mut buf = BytesMut::new();
bacnet_encoding::constructed::encode_destination_list(&mut buf, destinations).unwrap();
buf.to_vec()
}
fn nc_db(entries: &[BACnetDestination]) -> (ObjectDatabase, ObjectIdentifier) {
let mut db = ObjectDatabase::new();
let mut nc = NotificationClass::new(1, "NC-1").unwrap();
for entry in entries {
nc.add_destination(entry.clone()).unwrap();
}
let oid = nc.object_identifier();
db.add(Box::new(nc)).unwrap();
(db, oid)
}
fn edit(
db: &mut ObjectDatabase,
oid: ObjectIdentifier,
list_of_elements: Vec<u8>,
remove: bool,
) -> Result<(), Error> {
let mut buf = BytesMut::new();
ListElementRequest {
object_identifier: oid,
property_identifier: PropertyIdentifier::RECIPIENT_LIST,
property_array_index: None,
list_of_elements,
}
.encode(&mut buf)
.unwrap();
if remove {
handle_remove_list_element(db, &buf)
} else {
handle_add_list_element(db, &buf)
}
}
fn recipient_list_wire_bytes(db: &ObjectDatabase, oid: ObjectIdentifier) -> Vec<u8> {
let PropertyValue::ApplicationData(bytes) = db
.get(&oid)
.unwrap()
.read_property(PropertyIdentifier::RECIPIENT_LIST, None)
.unwrap()
else {
panic!("expected ApplicationData");
};
bytes
}
fn device_instances(db: &ObjectDatabase, oid: ObjectIdentifier) -> Vec<u32> {
bacnet_encoding::constructed::decode_destination_list(&recipient_list_wire_bytes(db, oid))
.unwrap()
.iter()
.map(|d| match &d.recipient {
BACnetRecipient::Device(o) => o.instance_number(),
other => panic!("expected Device recipient, got {other:?}"),
})
.collect()
}
#[test]
fn remove_list_element_from_framed_recipient_list_leaves_rest() {
let (mut db, oid) = nc_db(&[destination(10), destination(20), destination(30)]);
edit(&mut db, oid, framed(&[destination(20)]), true).unwrap();
assert_eq!(device_instances(&db, oid), vec![10, 30]);
assert_eq!(
recipient_list_wire_bytes(&db, oid),
framed(&[destination(10), destination(30)])
);
}
#[test]
fn remove_list_element_absent_recipient_fails_and_removes_nothing() {
let (mut db, oid) = nc_db(&[destination(10), destination(20)]);
let before = recipient_list_wire_bytes(&db, oid);
for (elements, position) in [
(vec![destination(99)], 1),
(vec![destination(20), destination(99)], 2),
] {
assert_eq!(
list_refusal(edit(&mut db, oid, framed(&elements), true)),
(
ErrorClass::SERVICES,
ErrorCode::LIST_ELEMENT_NOT_FOUND,
position
)
);
assert_eq!(
recipient_list_wire_bytes(&db, oid),
before,
"bytes unchanged"
);
}
}
#[test]
fn recipient_list_elements_compare_as_whole_destinations() {
let (mut db, oid) = nc_db(&[destination(10)]);
let mut other_process = destination(10);
other_process.process_identifier = 11;
let mut fewer_transitions = destination(10);
fewer_transitions.transitions =
EventTransitionBits::TO_OFFNORMAL | EventTransitionBits::TO_FAULT;
for variant in [&other_process, &fewer_transitions] {
assert_eq!(
list_refusal(edit(
&mut db,
oid,
framed(std::slice::from_ref(variant)),
true
)),
(ErrorClass::SERVICES, ErrorCode::LIST_ELEMENT_NOT_FOUND, 1)
);
}
edit(
&mut db,
oid,
framed(&[
destination(10),
other_process.clone(),
other_process.clone(),
]),
false,
)
.unwrap();
assert_eq!(
recipient_list_wire_bytes(&db, oid),
framed(&[destination(10), other_process])
);
}
#[test]
fn add_list_element_to_framed_recipient_list_appends() {
let (mut db, oid) = nc_db(&[destination(10)]);
edit(
&mut db,
oid,
framed(&[destination(20), destination(30)]),
false,
)
.unwrap();
assert_eq!(device_instances(&db, oid), vec![10, 20, 30]);
assert_eq!(
recipient_list_wire_bytes(&db, oid),
framed(&[destination(10), destination(20), destination(30)])
);
edit(&mut db, oid, framed(&[destination(20)]), false).unwrap();
assert_eq!(device_instances(&db, oid), vec![10, 20, 30]);
}
#[test]
fn remove_list_element_malformed_framed_payload_errors_and_preserves() {
let (mut db, oid) = nc_db(&[destination(10), destination(20)]);
let before = recipient_list_wire_bytes(&db, oid);
let mut elements = framed(&[destination(10)]);
elements.extend_from_slice(&[0x21, 0x2A]);
for remove in [true, false] {
assert_eq!(
list_refusal(edit(&mut db, oid, elements.clone(), remove)),
(ErrorClass::PROPERTY, ErrorCode::INVALID_DATA_TYPE, 2)
);
assert_eq!(
recipient_list_wire_bytes(&db, oid),
before,
"no silent wipe"
);
}
}