use super::*;
use PropertyIdentifier as P;
use bacnet_objects::access_control::{
AccessDoorObject, AccessPointObject, CredentialDataInputObject,
};
use bacnet_types::constructed::{
BACnetAuthenticationFactorFormat, BACnetDeviceObjectReference, PropertyReference,
ReadAccessSpecification,
};
use bacnet_types::enums::AuthenticationFactorType;
use bacnet_types::error::RemoteError;
pub(super) type Expected = Result<Vec<u8>, ErrorCode>;
pub(super) fn assert_reads(
db: &ObjectDatabase,
oid: ObjectIdentifier,
cases: &[(P, Option<u32>, Expected)],
) {
let mut request = BytesMut::new();
ReadPropertyMultipleRequest {
list_of_read_access_specs: vec![ReadAccessSpecification {
object_identifier: oid,
list_of_property_references: cases
.iter()
.map(
|&(property_identifier, property_array_index, _)| PropertyReference {
property_identifier,
property_array_index,
},
)
.collect(),
}],
}
.encode(&mut request)
.unwrap();
let mut response = BytesMut::new();
handle_read_property_multiple(db, &request, &mut response).unwrap();
let ack = ReadPropertyMultipleACK::decode(&response).unwrap();
let results = &ack.list_of_read_access_results[0].list_of_results;
assert_eq!(results.len(), cases.len());
for (result, (property, index, expected)) in results.iter().zip(cases) {
assert_eq!(result.property_identifier, *property);
assert_eq!(result.property_array_index, *index);
let mut rp_request = BytesMut::new();
ReadPropertyRequest {
object_identifier: oid,
property_identifier: *property,
property_array_index: *index,
}
.encode(&mut rp_request);
let mut rp_response = BytesMut::new();
let rp = handle_read_property(db, &rp_request, &mut rp_response);
match expected {
Ok(bytes) => {
assert_eq!(
result.property_value.as_deref(),
Some(bytes.as_slice()),
"{property:?} {index:?}"
);
rp.unwrap();
let rp_ack = ReadPropertyACK::decode(&rp_response).unwrap();
assert_eq!(rp_ack.property_array_index, *index);
assert_eq!(rp_ack.property_value, *bytes, "{property:?} {index:?}");
}
Err(code) => {
assert_eq!(
result.error,
Some((ErrorClass::PROPERTY, *code)),
"{property:?} {index:?}"
);
assert!(
matches!(rp, Err(Error::Remote(RemoteError::Error { class, code: actual }))
if class == ErrorClass::PROPERTY
&& actual == *code),
"{property:?} {index:?}: {rp:?}"
);
}
}
}
}
pub(super) fn array_cases(property: P, elements: &[&[u8]]) -> Vec<(P, Option<u32>, Expected)> {
let size = elements.len() as u32;
let mut cases = vec![
(property, None, Ok(elements.concat())),
(property, Some(0), Ok(vec![0x21, size as u8])),
];
if let (Some(first), Some(last)) = (elements.first(), elements.last()) {
cases.push((property, Some(1), Ok(first.to_vec())));
cases.push((property, Some(size), Ok(last.to_vec())));
}
cases.push((
property,
Some(size + 1),
Err(ErrorCode::INVALID_ARRAY_INDEX),
));
cases
}
pub(super) fn db_with(object: Box<dyn BACnetObject>) -> (ObjectDatabase, ObjectIdentifier) {
let oid = object.object_identifier();
let mut db = ObjectDatabase::new();
db.add(object).unwrap();
(db, oid)
}
fn reference(
device: Option<u32>,
object_type: ObjectType,
instance: u32,
) -> BACnetDeviceObjectReference {
BACnetDeviceObjectReference {
device_identifier: device
.map(|device| ObjectIdentifier::new(ObjectType::DEVICE, device).unwrap()),
object_identifier: ObjectIdentifier::new(object_type, instance).unwrap(),
}
}
#[test]
fn credential_data_input_format_arrays_read_per_index() {
let empty = CredentialDataInputObject::new(1, "CDI-1").unwrap();
let (db, oid) = db_with(Box::new(empty));
let mut cases = array_cases(P::SUPPORTED_FORMATS, &[]);
cases.extend(array_cases(P::SUPPORTED_FORMAT_CLASSES, &[]));
assert_reads(&db, oid, &cases);
let mut reader = CredentialDataInputObject::new(1, "CDI-1").unwrap();
reader
.set_supported_formats([
(
BACnetAuthenticationFactorFormat::standard(AuthenticationFactorType::WIEGAND26),
0,
),
(
BACnetAuthenticationFactorFormat::standard(AuthenticationFactorType::FASC_N),
1,
),
(BACnetAuthenticationFactorFormat::custom(260, 7), 3),
])
.unwrap();
let (db, oid) = db_with(Box::new(reader));
let mut cases = array_cases(
P::SUPPORTED_FORMATS,
&[
&[0x09, 0x08],
&[0x09, 0x0D],
&[0x09, 0x02, 0x1A, 0x01, 0x04, 0x29, 0x07],
],
);
cases.extend(array_cases(
P::SUPPORTED_FORMAT_CLASSES,
&[&[0x21, 0x00], &[0x21, 0x01], &[0x21, 0x03]],
));
assert_reads(&db, oid, &cases);
}
#[test]
fn access_door_door_members_read_per_index() {
let (db, oid) = db_with(Box::new(AccessDoorObject::new(1, "DOOR-1").unwrap()));
assert_reads(&db, oid, &array_cases(P::DOOR_MEMBERS, &[]));
let mut door = AccessDoorObject::new(1, "DOOR-1").unwrap();
door.set_door_members([
reference(None, ObjectType::BINARY_INPUT, 3),
reference(None, ObjectType::BINARY_OUTPUT, 4),
reference(Some(99), ObjectType::CREDENTIAL_DATA_INPUT, 1),
])
.unwrap();
let (db, oid) = db_with(Box::new(door));
assert_reads(
&db,
oid,
&array_cases(
P::DOOR_MEMBERS,
&[
&[0x1C, 0x00, 0xC0, 0x00, 0x03],
&[0x1C, 0x01, 0x00, 0x00, 0x04],
&[0x0C, 0x02, 0x00, 0x00, 0x63, 0x1C, 0x09, 0x40, 0x00, 0x01],
],
),
);
}
#[test]
fn access_point_access_doors_read_per_index() {
let (db, oid) = db_with(Box::new(AccessPointObject::new(1, "AP-1").unwrap()));
assert_reads(&db, oid, &array_cases(P::ACCESS_DOORS, &[]));
let mut point = AccessPointObject::new(1, "AP-1").unwrap();
point
.set_access_doors([
reference(None, ObjectType::ACCESS_DOOR, 1),
reference(Some(99), ObjectType::ACCESS_DOOR, 2),
])
.unwrap();
let (mut db, oid) = db_with(Box::new(point));
assert_reads(
&db,
oid,
&array_cases(
P::ACCESS_DOORS,
&[
&[0x1C, 0x07, 0x80, 0x00, 0x01],
&[0x0C, 0x02, 0x00, 0x00, 0x63, 0x1C, 0x07, 0x80, 0x00, 0x02],
],
),
);
let mut request = BytesMut::new();
WritePropertyRequest {
object_identifier: oid,
property_identifier: P::ACCESS_DOORS,
property_array_index: Some(1),
property_value: vec![0x1C, 0x07, 0x80, 0x00, 0x05],
priority: None,
}
.encode(&mut request)
.unwrap();
assert!(matches!(
handle_write_property(&mut db, &request),
Err(Error::Remote(RemoteError::Error { class, code }))
if class == ErrorClass::PROPERTY
&& code == ErrorCode::WRITE_ACCESS_DENIED
));
}