use bacnet_types::constructed::{BACnetDeviceObjectPropertyReference, BACnetDeviceObjectReference};
use bacnet_types::enums::{ErrorClass, ErrorCode, PropertyIdentifier as P};
use super::*;
fn oid(object_type: ObjectType, instance: u32) -> ObjectIdentifier {
ObjectIdentifier::new(object_type, instance).unwrap()
}
fn assert_property_error<T: std::fmt::Debug>(result: Result<T, Error>, code: ErrorCode) {
assert!(
matches!(result, Err(Error::Protocol { class, code: actual })
if class == ErrorClass::PROPERTY.to_raw() as u32
&& actual == code.to_raw() as u32),
"expected PROPERTY / {code:?}, got {result:?}"
);
}
fn schedule(instance: u32) -> BACnetDeviceObjectPropertyReference {
BACnetDeviceObjectPropertyReference::new_local(oid(ObjectType::SCHEDULE, instance), 85)
}
fn local(object_type: ObjectType, instance: u32) -> BACnetDeviceObjectReference {
oid(object_type, instance).into()
}
fn rule(
time_range: Option<BACnetDeviceObjectPropertyReference>,
location: Option<BACnetDeviceObjectReference>,
) -> BACnetAccessRule {
BACnetAccessRule::new(time_range, location, true)
}
fn element(rule: &BACnetAccessRule) -> PropertyValue {
let mut buf = BytesMut::new();
encode_access_rule(&mut buf, rule);
PropertyValue::ApplicationData(buf.to_vec())
}
#[test]
fn access_rights_rule_arrays_read_whole_and_by_index() {
let mut rights = AccessRightsObject::new(7, "AR-7").unwrap();
let business_hours = rule(Some(schedule(1)), Some(local(ObjectType::ACCESS_POINT, 2)));
let anywhere = BACnetAccessRule::new(None, None, false);
let lockdown = rule(None, Some(local(ObjectType::ACCESS_ZONE, 3)));
rights
.set_positive_access_rules([business_hours.clone(), anywhere.clone()])
.unwrap();
rights
.set_negative_access_rules([lockdown.clone()])
.unwrap();
assert_eq!(
rights.positive_access_rules(),
[business_hours.clone(), anywhere.clone()]
);
assert_eq!(
rights.negative_access_rules(),
std::slice::from_ref(&lockdown)
);
for (property, rules) in [
(P::POSITIVE_ACCESS_RULES, vec![business_hours, anywhere]),
(P::NEGATIVE_ACCESS_RULES, vec![lockdown]),
] {
assert!(rights.is_array_property(property), "{property:?}");
let elements: Vec<PropertyValue> = rules.iter().map(element).collect();
assert_eq!(
rights.read_property(property, None).unwrap(),
PropertyValue::List(elements.clone())
);
assert_eq!(
rights.read_property(property, Some(0)).unwrap(),
PropertyValue::Unsigned(rules.len() as u64)
);
for (index, element) in elements.into_iter().enumerate() {
assert_eq!(
rights
.read_property(property, Some(index as u32 + 1))
.unwrap(),
element
);
}
assert_property_error(
rights.read_property(property, Some(rules.len() as u32 + 1)),
ErrorCode::INVALID_ARRAY_INDEX,
);
}
assert_eq!(
rights
.read_property(P::POSITIVE_ACCESS_RULES, Some(2))
.unwrap(),
PropertyValue::ApplicationData(vec![0x09, 0x01, 0x29, 0x01, 0x49, 0x00])
);
}
#[test]
fn access_rights_setters_refuse_ill_formed_rules_and_keep_the_old_ones() {
let unspecified_time_range = schedule(ObjectIdentifier::MAX_INSTANCE);
let unspecified_location = BACnetDeviceObjectReference {
device_identifier: Some(oid(ObjectType::DEVICE, ObjectIdentifier::MAX_INSTANCE)),
object_identifier: oid(ObjectType::ACCESS_POINT, ObjectIdentifier::MAX_INSTANCE),
};
let with = |time_range_specifier, time_range, location_specifier, location| BACnetAccessRule {
time_range_specifier,
time_range,
location_specifier,
location,
enable: true,
};
use AccessRuleLocationSpecifier as L;
use AccessRuleTimeRangeSpecifier as T;
let point = || Some(local(ObjectType::ACCESS_POINT, 2));
let refused = [
with(T::SPECIFIED, None, L::SPECIFIED, point()),
with(T::ALWAYS, None, L::SPECIFIED, None),
with(T::ALWAYS, Some(schedule(1)), L::ALL, None),
with(T::ALWAYS, None, L::ALL, point()),
with(
T::ALWAYS,
Some(BACnetDeviceObjectPropertyReference::new_remote(
oid(ObjectType::SCHEDULE, ObjectIdentifier::MAX_INSTANCE),
85,
oid(ObjectType::DEVICE, 99),
)),
L::ALL,
None,
),
with(T::from_raw(2), None, L::ALL, None),
with(T::ALWAYS, None, L::from_raw(2), None),
rule(None, Some(local(ObjectType::ACCESS_DOOR, 2))),
rule(None, Some(local(ObjectType::ANALOG_VALUE, 2))),
];
let accepted = [
rule(Some(schedule(1)), point()),
with(
T::ALWAYS,
Some(unspecified_time_range.clone()),
L::ALL,
None,
),
with(T::SPECIFIED, Some(unspecified_time_range), L::ALL, None),
with(T::ALWAYS, None, L::ALL, Some(unspecified_location.clone())),
with(T::ALWAYS, None, L::SPECIFIED, Some(unspecified_location)),
with(
T::ALWAYS,
None,
L::SPECIFIED,
Some(local(
ObjectType::ANALOG_VALUE,
ObjectIdentifier::MAX_INSTANCE,
)),
),
rule(
Some(BACnetDeviceObjectPropertyReference::new_local(
oid(ObjectType::BINARY_VALUE, 4),
85,
)),
Some(BACnetDeviceObjectReference {
device_identifier: Some(oid(ObjectType::DEVICE, 99)),
object_identifier: oid(ObjectType::ACCESS_ZONE, 3),
}),
),
];
let mut rights = AccessRightsObject::new(7, "AR-7").unwrap();
rights.set_positive_access_rules(accepted.clone()).unwrap();
rights.set_negative_access_rules(accepted.clone()).unwrap();
for bad in refused {
let list = [accepted[0].clone(), bad.clone()];
assert_property_error(
rights.set_positive_access_rules(list.clone()),
ErrorCode::VALUE_OUT_OF_RANGE,
);
assert_property_error(
rights.set_negative_access_rules(list),
ErrorCode::VALUE_OUT_OF_RANGE,
);
assert_eq!(rights.positive_access_rules(), accepted, "{bad:?}");
assert_eq!(rights.negative_access_rules(), accepted, "{bad:?}");
}
rights.set_negative_access_rules([]).unwrap();
assert_eq!(
rights
.read_property(P::NEGATIVE_ACCESS_RULES, Some(0))
.unwrap(),
PropertyValue::Unsigned(0)
);
}
#[test]
fn access_rights_rules_refuse_a_non_device_device_identifier() {
let not_a_device = oid(ObjectType::ANALOG_VALUE, 99);
let time_range = BACnetDeviceObjectPropertyReference::new_remote(
oid(ObjectType::SCHEDULE, 1),
85,
not_a_device,
);
let location = BACnetDeviceObjectReference {
device_identifier: Some(not_a_device),
object_identifier: oid(ObjectType::ACCESS_POINT, 2),
};
let mut rights = AccessRightsObject::new(7, "AR-7").unwrap();
for bad in [rule(Some(time_range), None), rule(None, Some(location))] {
assert_property_error(
rights.set_positive_access_rules([bad.clone()]),
ErrorCode::VALUE_OUT_OF_RANGE,
);
assert_property_error(
rights.set_negative_access_rules([bad]),
ErrorCode::VALUE_OUT_OF_RANGE,
);
}
assert!(rights.positive_access_rules().is_empty());
assert!(rights.negative_access_rules().is_empty());
}
#[test]
fn access_rights_setters_refuse_more_rules_than_the_cap() {
let kept = rule(Some(schedule(1)), None);
let mut rights = AccessRightsObject::new(7, "AR-7").unwrap();
rights.set_positive_access_rules([kept.clone()]).unwrap();
rights.set_negative_access_rules([kept.clone()]).unwrap();
let too_many = vec![BACnetAccessRule::new(None, None, true); MAX_ACCESS_RULES + 1];
for result in [
rights.set_positive_access_rules(too_many.clone()),
rights.set_negative_access_rules(too_many),
] {
assert!(
matches!(result, Err(Error::Protocol { class, code })
if class == ErrorClass::RESOURCES.to_raw() as u32
&& code == ErrorCode::NO_SPACE_TO_WRITE_PROPERTY.to_raw() as u32),
"{result:?}"
);
}
assert_eq!(rights.positive_access_rules(), std::slice::from_ref(&kept));
assert_eq!(rights.negative_access_rules(), std::slice::from_ref(&kept));
let full = vec![BACnetAccessRule::new(None, None, true); MAX_ACCESS_RULES];
rights.set_positive_access_rules(full).unwrap();
assert_eq!(
rights
.read_property(P::POSITIVE_ACCESS_RULES, Some(0))
.unwrap(),
PropertyValue::Unsigned(MAX_ACCESS_RULES as u64)
);
}
#[test]
fn access_rights_enable_defaults_true_and_follows_the_setter_and_writes() {
let mut rights = AccessRightsObject::new(7, "AR-7").unwrap();
assert!(rights.enable());
assert_eq!(
rights.read_property(P::LOG_ENABLE, None).unwrap(),
PropertyValue::Boolean(true)
);
assert!(rights.property_list().contains(&P::LOG_ENABLE));
assert!(rights.is_writable_property(P::LOG_ENABLE));
let kept = rule(Some(schedule(1)), Some(local(ObjectType::ACCESS_POINT, 2)));
rights.set_positive_access_rules([kept.clone()]).unwrap();
rights.set_enable(false);
assert!(!rights.enable());
assert_eq!(
rights.read_property(P::LOG_ENABLE, None).unwrap(),
PropertyValue::Boolean(false)
);
assert_eq!(rights.positive_access_rules(), std::slice::from_ref(&kept));
rights
.write_property(P::LOG_ENABLE, None, PropertyValue::Boolean(true), None)
.unwrap();
assert!(rights.enable());
for wrong in [PropertyValue::Unsigned(0), PropertyValue::Enumerated(0)] {
assert_property_error(
rights.write_property(P::LOG_ENABLE, None, wrong, None),
ErrorCode::INVALID_DATA_TYPE,
);
}
assert!(rights.enable());
}