use bacnet_encoding::constructed::encode_authentication_policy;
use bacnet_types::constructed::{BACnetAuthenticationPolicy, BACnetAuthenticationPolicyEntry};
use bacnet_types::enums::{ErrorClass, ErrorCode, PropertyIdentifier as P};
use super::*;
const LIST: P = P::AUTHENTICATION_POLICY_LIST;
const NAMES: P = P::AUTHENTICATION_POLICY_NAMES;
const POLICY: P = P::ACTIVE_AUTHENTICATION_POLICY;
const POLICIES: P = P::NUMBER_OF_AUTHENTICATION_POLICIES;
fn read(point: &AccessPointObject, property: P) -> PropertyValue {
point.read_property(property, None).unwrap()
}
fn write(point: &mut AccessPointObject, property: P, value: PropertyValue) -> Result<(), Error> {
point.write_property(property, None, value, None)
}
fn assert_error(result: Result<(), Error>, expected: ErrorCode) {
assert!(
matches!(result, Err(Error::Protocol { class, code })
if class == ErrorClass::PROPERTY.to_raw() as u32
&& code == expected.to_raw() as u32),
"expected {expected:?}, got {result:?}"
);
}
fn oid(object_type: ObjectType, instance: u32) -> ObjectIdentifier {
ObjectIdentifier::new(object_type, instance).unwrap()
}
fn entry(instance: u32, index: u32) -> BACnetAuthenticationPolicyEntry {
BACnetAuthenticationPolicyEntry {
credential_data_input: oid(ObjectType::CREDENTIAL_DATA_INPUT, instance).into(),
index,
}
}
fn policy(entries: Vec<BACnetAuthenticationPolicyEntry>) -> BACnetAuthenticationPolicy {
BACnetAuthenticationPolicy {
policy: entries,
order_enforced: true,
timeout: 30,
}
}
fn card_and_pin() -> Vec<(&'static str, BACnetAuthenticationPolicy)> {
vec![
("card", policy(vec![entry(1, 1)])),
("card and PIN", policy(vec![entry(1, 1), entry(2, 2)])),
]
}
fn encoded(policy: &BACnetAuthenticationPolicy) -> PropertyValue {
let mut buf = BytesMut::new();
encode_authentication_policy(&mut buf, policy);
PropertyValue::ApplicationData(buf.to_vec())
}
fn reliability(point: &AccessPointObject) -> Reliability {
let PropertyValue::Enumerated(raw) = read(point, P::RELIABILITY) else {
panic!("Reliability is an Enumerated");
};
Reliability::from_raw(raw)
}
fn fault(point: &AccessPointObject) -> bool {
let PropertyValue::BitString { data, .. } = read(point, P::STATUS_FLAGS) else {
panic!("Status_Flags is a BIT STRING");
};
data[0] & 0x40 != 0
}
#[test]
fn access_point_serves_the_policy_arrays_once_the_application_sets_them() {
let mut point = AccessPointObject::new(1, "AP-1").unwrap();
for property in [LIST, NAMES] {
assert!(!point.property_list().contains(&property));
assert!(point.read_property(property, None).is_err());
assert!(point.is_array_property(property));
}
let policies = card_and_pin();
point.set_authentication_policies(policies.clone()).unwrap();
assert_eq!(read(&point, POLICIES), PropertyValue::Unsigned(2));
assert_eq!(
read(&point, LIST),
PropertyValue::List(policies.iter().map(|(_, p)| encoded(p)).collect())
);
assert_eq!(
read(&point, NAMES),
PropertyValue::List(vec![
PropertyValue::CharacterString("card".into()),
PropertyValue::CharacterString("card and PIN".into()),
])
);
for property in [LIST, NAMES] {
assert_eq!(
point.read_property(property, Some(0)).unwrap(),
PropertyValue::Unsigned(2)
);
assert!(matches!(
point.read_property(property, Some(3)),
Err(Error::Protocol { code, .. }) if code == ErrorCode::INVALID_ARRAY_INDEX.to_raw() as u32
));
}
assert_eq!(
point.read_property(LIST, Some(2)).unwrap(),
encoded(&policies[1].1)
);
assert_eq!(
point.read_property(NAMES, Some(1)).unwrap(),
PropertyValue::CharacterString("card".into())
);
let list = point.property_list();
assert_eq!(&list[list.len() - 2..], &[LIST, NAMES]);
for property in [LIST, NAMES] {
assert!(!point.required_properties().contains(&property));
assert!(!point.is_writable_property(property));
}
assert_eq!(read(&point, POLICY), PropertyValue::Unsigned(1));
assert_eq!(reliability(&point), Reliability::NO_FAULT_DETECTED);
}
#[test]
fn access_point_policy_arrays_and_count_refuse_network_writes() {
let mut point = AccessPointObject::new(1, "AP-1").unwrap();
point.set_authentication_policies(card_and_pin()).unwrap();
let before = [
read(&point, LIST),
read(&point, NAMES),
read(&point, POLICIES),
];
for (property, value) in [
(LIST, read(&point, LIST)),
(NAMES, read(&point, NAMES)),
(POLICIES, PropertyValue::Unsigned(1)),
] {
assert_error(
write(&mut point, property, value),
ErrorCode::WRITE_ACCESS_DENIED,
);
}
assert_error(
point.write_property(
NAMES,
Some(1),
PropertyValue::CharacterString("x".into()),
None,
),
ErrorCode::WRITE_ACCESS_DENIED,
);
assert_eq!(
[
read(&point, LIST),
read(&point, NAMES),
read(&point, POLICIES)
],
before
);
let none: Vec<(&str, BACnetAuthenticationPolicy)> = Vec::new();
assert_error(
point.set_authentication_policies(none),
ErrorCode::VALUE_OUT_OF_RANGE,
);
assert_eq!(
[
read(&point, LIST),
read(&point, NAMES),
read(&point, POLICIES)
],
before
);
}
#[test]
fn access_point_invalid_policy_in_effect_drops_to_zero_and_faults() {
let remote_device = BACnetDeviceObjectReference {
device_identifier: Some(oid(ObjectType::DEVICE, 9)),
object_identifier: oid(ObjectType::CREDENTIAL_DATA_INPUT, 3),
};
let not_a_device = BACnetDeviceObjectReference {
device_identifier: Some(oid(ObjectType::ANALOG_VALUE, 9)),
object_identifier: oid(ObjectType::CREDENTIAL_DATA_INPUT, 3),
};
let malformed = [
vec![],
vec![BACnetAuthenticationPolicyEntry {
credential_data_input: oid(ObjectType::ACCESS_POINT, 1).into(),
index: 1,
}],
vec![BACnetAuthenticationPolicyEntry {
credential_data_input: not_a_device,
index: 1,
}],
vec![entry(1, 0)],
vec![entry(1, 2)],
vec![entry(1, 1), entry(2, 3)],
vec![entry(1, 1), entry(2, 2), entry(3, 1)],
];
for entries in malformed {
let mut point = AccessPointObject::new(1, "AP-1").unwrap();
point
.set_authentication_policies([
("bad", policy(entries.clone())),
card_and_pin()[0].clone(),
])
.unwrap();
assert_eq!(
read(&point, POLICY),
PropertyValue::Unsigned(0),
"{entries:?}"
);
assert_eq!(reliability(&point), Reliability::CONFIGURATION_ERROR);
assert!(fault(&point));
assert_error(
write(&mut point, POLICY, PropertyValue::Unsigned(1)),
ErrorCode::VALUE_OUT_OF_RANGE,
);
assert_eq!(read(&point, POLICY), PropertyValue::Unsigned(0));
write(&mut point, POLICY, PropertyValue::Unsigned(2)).unwrap();
assert_eq!(read(&point, POLICY), PropertyValue::Unsigned(2));
assert_eq!(reliability(&point), Reliability::CONFIGURATION_ERROR);
point.set_authentication_policies(card_and_pin()).unwrap();
assert_eq!(read(&point, POLICY), PropertyValue::Unsigned(2));
assert_eq!(reliability(&point), Reliability::NO_FAULT_DETECTED);
assert!(!fault(&point));
}
for entries in [
vec![
entry(1, 1),
entry(2, 1),
entry(3, 2),
entry(4, 2),
entry(5, 3),
],
vec![BACnetAuthenticationPolicyEntry {
credential_data_input: remote_device,
index: 1,
}],
] {
let mut point = AccessPointObject::new(1, "AP-1").unwrap();
let unordered = BACnetAuthenticationPolicy {
policy: entries.clone(),
order_enforced: false,
timeout: 0,
};
point
.set_authentication_policies([("ok", unordered)])
.unwrap();
assert_eq!(
read(&point, POLICY),
PropertyValue::Unsigned(1),
"{entries:?}"
);
assert_eq!(reliability(&point), Reliability::NO_FAULT_DETECTED);
}
}
#[test]
fn access_point_policy_arrays_follow_the_count() {
let mut point = AccessPointObject::new(1, "AP-1").unwrap();
point.set_authentication_policies(card_and_pin()).unwrap();
write(&mut point, POLICY, PropertyValue::Unsigned(2)).unwrap();
point.set_number_of_authentication_policies(3).unwrap();
assert_eq!(reliability(&point), Reliability::CONFIGURATION_ERROR);
assert_eq!(
point.read_property(LIST, Some(3)).unwrap(),
encoded(&BACnetAuthenticationPolicy::default())
);
assert_eq!(
point.read_property(NAMES, Some(3)).unwrap(),
PropertyValue::CharacterString(String::new())
);
for property in [LIST, NAMES] {
assert_eq!(
point.read_property(property, Some(0)).unwrap(),
PropertyValue::Unsigned(3)
);
}
assert_eq!(read(&point, POLICY), PropertyValue::Unsigned(2));
assert_error(
write(&mut point, POLICY, PropertyValue::Unsigned(3)),
ErrorCode::VALUE_OUT_OF_RANGE,
);
point.set_number_of_authentication_policies(1).unwrap();
assert_eq!(
read(&point, NAMES),
PropertyValue::List(vec![PropertyValue::CharacterString("card".into())])
);
assert_eq!(read(&point, POLICY), PropertyValue::Unsigned(0));
assert_eq!(reliability(&point), Reliability::CONFIGURATION_ERROR);
point.set_authentication_policies(card_and_pin()).unwrap();
assert_eq!(read(&point, POLICY), PropertyValue::Unsigned(0));
write(&mut point, POLICY, PropertyValue::Unsigned(1)).unwrap();
assert_eq!(reliability(&point), Reliability::NO_FAULT_DETECTED);
}
#[test]
fn access_point_reliability_takes_simulated_writes_only_out_of_service() {
let mut point = AccessPointObject::new(1, "AP-1").unwrap();
let simulated = PropertyValue::Enumerated(Reliability::UNRELIABLE_OTHER.to_raw());
assert_error(
write(&mut point, P::RELIABILITY, simulated.clone()),
ErrorCode::WRITE_ACCESS_DENIED,
);
assert_eq!(reliability(&point), Reliability::NO_FAULT_DETECTED);
write(&mut point, P::OUT_OF_SERVICE, PropertyValue::Boolean(true)).unwrap();
write(&mut point, P::RELIABILITY, simulated).unwrap();
assert_eq!(reliability(&point), Reliability::UNRELIABLE_OTHER);
assert!(fault(&point));
for (value, code) in [
(PropertyValue::Unsigned(1), ErrorCode::INVALID_DATA_TYPE),
(
PropertyValue::Enumerated(70_000),
ErrorCode::VALUE_OUT_OF_RANGE,
),
] {
assert_error(write(&mut point, P::RELIABILITY, value), code);
}
assert_eq!(reliability(&point), Reliability::UNRELIABLE_OTHER);
point
.set_authentication_policies([("empty", BACnetAuthenticationPolicy::default())])
.unwrap();
assert_eq!(read(&point, POLICY), PropertyValue::Unsigned(0));
assert_eq!(reliability(&point), Reliability::UNRELIABLE_OTHER);
write(
&mut point,
P::RELIABILITY,
PropertyValue::Enumerated(Reliability::NO_FAULT_DETECTED.to_raw()),
)
.unwrap();
assert!(!fault(&point));
write(&mut point, P::OUT_OF_SERVICE, PropertyValue::Boolean(false)).unwrap();
assert_eq!(reliability(&point), Reliability::CONFIGURATION_ERROR);
assert!(fault(&point));
write(&mut point, P::OUT_OF_SERVICE, PropertyValue::Boolean(true)).unwrap();
assert_eq!(reliability(&point), Reliability::CONFIGURATION_ERROR);
}
#[test]
fn access_point_caps_the_policy_count_while_it_serves_the_arrays() {
let names = |count: u32| (0..count).map(|n| (format!("p{n}"), policy(vec![entry(1, 1)])));
let mut point = AccessPointObject::new(1, "AP-1").unwrap();
point
.set_number_of_authentication_policies(u32::MAX)
.unwrap();
assert_error(
point.set_authentication_policies(names(MAX_AUTHENTICATION_POLICIES + 1)),
ErrorCode::VALUE_OUT_OF_RANGE,
);
assert!(!point.property_list().contains(&LIST));
point
.set_authentication_policies(names(MAX_AUTHENTICATION_POLICIES))
.unwrap();
assert_eq!(
read(&point, POLICIES),
PropertyValue::Unsigned(MAX_AUTHENTICATION_POLICIES.into())
);
for count in [MAX_AUTHENTICATION_POLICIES + 1, u32::MAX] {
assert_error(
point.set_number_of_authentication_policies(count),
ErrorCode::VALUE_OUT_OF_RANGE,
);
}
assert_eq!(
point.read_property(NAMES, Some(0)).unwrap(),
PropertyValue::Unsigned(MAX_AUTHENTICATION_POLICIES.into())
);
point.set_number_of_authentication_policies(2).unwrap();
}