use super::*;
use bacnet_objects::binary::BinaryValueObject;
use bacnet_objects::event_enrollment::EventEnrollmentObject;
use bacnet_types::error::RemoteError;
use bacnet_types::primitives::PropertyValue;
fn encode_name_write(oid: ObjectIdentifier, name: &str) -> Vec<u8> {
let mut value_buf = BytesMut::new();
bacnet_encoding::primitives::encode_app_character_string(&mut value_buf, name).unwrap();
let request = WritePropertyRequest {
object_identifier: oid,
property_identifier: PropertyIdentifier::OBJECT_NAME,
property_array_index: None,
property_value: value_buf.to_vec(),
priority: None,
};
let mut buf = BytesMut::new();
request.encode(&mut buf).unwrap();
buf.to_vec()
}
fn assert_duplicate_name<T: std::fmt::Debug>(result: Result<T, Error>) {
match result {
Err(Error::Remote(RemoteError::Error { class, code })) => assert_eq!(
(class, code),
(ErrorClass::PROPERTY, ErrorCode::DUPLICATE_NAME)
),
other => panic!("expected PROPERTY / DUPLICATE_NAME, got {other:?}"),
}
}
fn db_with_two_bvs() -> (ObjectDatabase, ObjectIdentifier, ObjectIdentifier) {
let mut db = ObjectDatabase::new();
let a = BinaryValueObject::new(1, "BV-A").unwrap();
let b = BinaryValueObject::new(2, "BV-B").unwrap();
db.add(Box::new(a)).unwrap();
db.add(Box::new(b)).unwrap();
let oid_a = ObjectIdentifier::new(ObjectType::BINARY_VALUE, 1).unwrap();
let oid_b = ObjectIdentifier::new(ObjectType::BINARY_VALUE, 2).unwrap();
(db, oid_a, oid_b)
}
#[test]
fn write_object_name_rejects_duplicate() {
let (mut db, oid_a, _oid_b) = db_with_two_bvs();
let buf = encode_name_write(oid_a, "BV-B");
assert_duplicate_name(handle_write_property(&mut db, &buf));
assert!(db.find_by_name("BV-A").is_some());
assert!(db.find_by_name("BV-B").is_some());
assert_eq!(
db.get(&oid_a).unwrap().object_name(),
"BV-A",
"rejected write must not mutate the object name"
);
}
#[test]
fn write_object_name_rename_refreshes_index() {
let (mut db, oid_a, oid_b) = db_with_two_bvs();
let buf = encode_name_write(oid_a, "BV-A2");
handle_write_property(&mut db, &buf).unwrap();
assert!(db.find_by_name("BV-A").is_none(), "old name must be freed");
let found = db.find_by_name("BV-A2").expect("new name must resolve");
assert_eq!(found.object_identifier(), oid_a);
assert_eq!(db.get(&oid_a).unwrap().object_name(), "BV-A2");
let c = BinaryValueObject::new(3, "BV-A").unwrap();
db.add(Box::new(c)).unwrap();
assert!(db.find_by_name("BV-A").is_some());
assert_eq!(db.get(&oid_b).unwrap().object_name(), "BV-B");
}
#[test]
fn event_enrollment_name_write_refreshes_index_and_rejects_duplicate() {
let mut db = ObjectDatabase::new();
let enrollment = EventEnrollmentObject::new(1, "EE-A", EventType::CHANGE_OF_BITSTRING).unwrap();
let occupied = BinaryValueObject::new(1, "Occupied").unwrap();
let oid = enrollment.object_identifier();
db.add(Box::new(enrollment)).unwrap();
db.add(Box::new(occupied)).unwrap();
handle_write_property(&mut db, &encode_name_write(oid, "EE-Renamed")).unwrap();
assert!(db.find_by_name("EE-A").is_none());
assert_eq!(
db.find_by_name("EE-Renamed").unwrap().object_identifier(),
oid
);
assert_duplicate_name(handle_write_property(
&mut db,
&encode_name_write(oid, "Occupied"),
));
assert_eq!(db.get(&oid).unwrap().object_name(), "EE-Renamed");
assert_eq!(
db.find_by_name("EE-Renamed").unwrap().object_identifier(),
oid
);
}
#[test]
fn write_object_name_empty_or_wrong_type_still_rejected() {
let (mut db, oid_a, _oid_b) = db_with_two_bvs();
let buf = encode_name_write(oid_a, "");
assert!(handle_write_property(&mut db, &buf).is_err());
assert_eq!(db.get(&oid_a).unwrap().object_name(), "BV-A");
let mut value_buf = BytesMut::new();
bacnet_encoding::primitives::encode_app_unsigned(&mut value_buf, 7);
let request = WritePropertyRequest {
object_identifier: oid_a,
property_identifier: PropertyIdentifier::OBJECT_NAME,
property_array_index: None,
property_value: value_buf.to_vec(),
priority: None,
};
let mut buf = BytesMut::new();
request.encode(&mut buf).unwrap();
assert!(handle_write_property(&mut db, &buf).is_err());
assert_eq!(db.get(&oid_a).unwrap().object_name(), "BV-A");
assert!(db.find_by_name("BV-A").is_some());
}
#[test]
fn write_property_multiple_name_rename_refreshes_index() {
let (mut db, oid_a, _oid_b) = db_with_two_bvs();
let mut name_buf = BytesMut::new();
bacnet_encoding::primitives::encode_app_character_string(&mut name_buf, "BV-A2").unwrap();
let mut desc_buf = BytesMut::new();
bacnet_encoding::primitives::encode_app_character_string(&mut desc_buf, "renamed").unwrap();
let request = WritePropertyMultipleRequest {
list_of_write_access_specs: vec![bacnet_services::wpm::WriteAccessSpecification {
object_identifier: oid_a,
list_of_properties: vec![
bacnet_services::common::BACnetPropertyValue {
property_identifier: PropertyIdentifier::OBJECT_NAME,
property_array_index: None,
value: name_buf.to_vec(),
priority: None,
},
bacnet_services::common::BACnetPropertyValue {
property_identifier: PropertyIdentifier::DESCRIPTION,
property_array_index: None,
value: desc_buf.to_vec(),
priority: None,
},
],
}],
};
let mut buf = BytesMut::new();
request.encode(&mut buf).unwrap();
handle_write_property_multiple(&mut db, &buf).unwrap();
assert!(db.find_by_name("BV-A").is_none());
assert_eq!(db.get(&oid_a).unwrap().object_name(), "BV-A2");
let desc = db
.get(&oid_a)
.unwrap()
.read_property(PropertyIdentifier::DESCRIPTION, None)
.unwrap();
match desc {
PropertyValue::CharacterString(s) => assert_eq!(s, "renamed"),
other => panic!("expected CharacterString, got {other:?}"),
}
}
#[test]
fn write_property_multiple_failed_attempt_is_mutation_free_and_prefix_index_stays_committed() {
let (mut db, oid_a, oid_b) = db_with_two_bvs();
let mut name_buf = BytesMut::new();
bacnet_encoding::primitives::encode_app_character_string(&mut name_buf, "BV-B").unwrap();
let request = WritePropertyMultipleRequest {
list_of_write_access_specs: vec![bacnet_services::wpm::WriteAccessSpecification {
object_identifier: oid_a,
list_of_properties: vec![bacnet_services::common::BACnetPropertyValue {
property_identifier: PropertyIdentifier::OBJECT_NAME,
property_array_index: None,
value: name_buf.to_vec(),
priority: None,
}],
}],
};
let mut buf = BytesMut::new();
request.encode(&mut buf).unwrap();
assert_duplicate_name(handle_write_property_multiple(&mut db, &buf));
assert_eq!(db.get(&oid_a).unwrap().object_name(), "BV-A");
assert_eq!(db.get(&oid_b).unwrap().object_name(), "BV-B");
assert!(db.find_by_name("BV-A").is_some());
assert!(db.find_by_name("BV-B").is_some());
let mut good_name_buf = BytesMut::new();
bacnet_encoding::primitives::encode_app_character_string(&mut good_name_buf, "BV-A2").unwrap();
let mut bad_type_buf = BytesMut::new();
bacnet_encoding::primitives::encode_app_enumerated(&mut bad_type_buf, 5);
let request = WritePropertyMultipleRequest {
list_of_write_access_specs: vec![bacnet_services::wpm::WriteAccessSpecification {
object_identifier: oid_a,
list_of_properties: vec![
bacnet_services::common::BACnetPropertyValue {
property_identifier: PropertyIdentifier::OBJECT_NAME,
property_array_index: None,
value: good_name_buf.to_vec(),
priority: None,
},
bacnet_services::common::BACnetPropertyValue {
property_identifier: PropertyIdentifier::OBJECT_TYPE,
property_array_index: None,
value: bad_type_buf.to_vec(),
priority: None,
},
],
}],
};
let mut buf = BytesMut::new();
request.encode(&mut buf).unwrap();
assert!(
handle_write_property_multiple(&mut db, &buf).is_err(),
"read-only write must fail the transaction"
);
assert_eq!(
db.get(&oid_a).unwrap().object_name(),
"BV-A2",
"the successful rename remains committed"
);
assert!(
db.find_by_name("BV-A2").is_some(),
"the committed new name stays indexed"
);
assert!(
db.find_by_name("BV-A").is_none(),
"the committed rename keeps the old name free"
);
}
#[test]
fn remove_after_rename_frees_current_name_only() {
let (mut db, oid_a, _oid_b) = db_with_two_bvs();
let buf = encode_name_write(oid_a, "BV-A2");
handle_write_property(&mut db, &buf).unwrap();
assert!(db.find_by_name("BV-A").is_none());
assert!(db.find_by_name("BV-A2").is_some());
db.remove(&oid_a).unwrap();
assert!(
db.find_by_name("BV-A2").is_none(),
"remove frees current name"
);
assert!(db.find_by_name("BV-A").is_none());
}
#[test]
fn write_object_name_noop_rename_to_same_name_succeeds() {
let (mut db, oid_a, _oid_b) = db_with_two_bvs();
let buf = encode_name_write(oid_a, "BV-A");
handle_write_property(&mut db, &buf).unwrap();
assert_eq!(db.get(&oid_a).unwrap().object_name(), "BV-A");
assert!(db.find_by_name("BV-A").is_some());
assert!(db.find_by_name("BV-B").is_some());
}
#[test]
fn write_property_multiple_cross_object_name_move() {
let (mut db, oid_a, oid_b) = db_with_two_bvs();
let mut a_buf = BytesMut::new();
bacnet_encoding::primitives::encode_app_character_string(&mut a_buf, "BV-A2").unwrap();
let mut b_buf = BytesMut::new();
bacnet_encoding::primitives::encode_app_character_string(&mut b_buf, "BV-A").unwrap();
let request = WritePropertyMultipleRequest {
list_of_write_access_specs: vec![
bacnet_services::wpm::WriteAccessSpecification {
object_identifier: oid_a,
list_of_properties: vec![bacnet_services::common::BACnetPropertyValue {
property_identifier: PropertyIdentifier::OBJECT_NAME,
property_array_index: None,
value: a_buf.to_vec(),
priority: None,
}],
},
bacnet_services::wpm::WriteAccessSpecification {
object_identifier: oid_b,
list_of_properties: vec![bacnet_services::common::BACnetPropertyValue {
property_identifier: PropertyIdentifier::OBJECT_NAME,
property_array_index: None,
value: b_buf.to_vec(),
priority: None,
}],
},
],
};
let mut buf = BytesMut::new();
request.encode(&mut buf).unwrap();
handle_write_property_multiple(&mut db, &buf).unwrap();
assert_eq!(db.get(&oid_a).unwrap().object_name(), "BV-A2");
assert_eq!(db.get(&oid_b).unwrap().object_name(), "BV-A");
assert_eq!(db.find_by_name("BV-A").unwrap().object_identifier(), oid_b);
assert_eq!(db.find_by_name("BV-A2").unwrap().object_identifier(), oid_a);
assert!(db.find_by_name("BV-B").is_none());
}
#[test]
fn create_object_with_object_name_initial_value_refreshes_index() {
let mut db = ObjectDatabase::new();
let mut name_buf = BytesMut::new();
bacnet_encoding::primitives::encode_app_character_string(&mut name_buf, "Custom-Name").unwrap();
let req = CreateObjectRequest {
object_specifier: ObjectSpecifier::Type(ObjectType::BINARY_VALUE),
list_of_initial_values: vec![bacnet_services::common::BACnetPropertyValue {
property_identifier: PropertyIdentifier::OBJECT_NAME,
property_array_index: None,
value: name_buf.to_vec(),
priority: None,
}],
};
let mut buf = BytesMut::new();
req.encode(&mut buf);
let mut ack_buf = BytesMut::new();
handle_create_object(&mut db, &buf, &mut ack_buf).unwrap();
let obj = db
.find_by_name("Custom-Name")
.expect("initial name resolves");
let created_oid = obj.object_identifier();
assert_eq!(db.get(&created_oid).unwrap().object_name(), "Custom-Name");
assert!(
db.find_by_name("BINARY_VALUE-1").is_none(),
"default name must be freed after rename"
);
}
#[test]
fn create_object_duplicate_object_name_initial_value_rejected_and_rolled_back() {
let (mut db, _oid_a, _oid_b) = db_with_two_bvs();
let before_len = db.len();
let mut name_buf = BytesMut::new();
bacnet_encoding::primitives::encode_app_character_string(&mut name_buf, "BV-B").unwrap();
let req = CreateObjectRequest {
object_specifier: ObjectSpecifier::Type(ObjectType::BINARY_VALUE),
list_of_initial_values: vec![bacnet_services::common::BACnetPropertyValue {
property_identifier: PropertyIdentifier::OBJECT_NAME,
property_array_index: None,
value: name_buf.to_vec(),
priority: None,
}],
};
let mut buf = BytesMut::new();
req.encode(&mut buf);
assert_eq!(
super::list_refusal(handle_create_object(&mut db, &buf, &mut BytesMut::new())),
(ErrorClass::PROPERTY, ErrorCode::DUPLICATE_NAME, 1)
);
assert_eq!(
db.len(),
before_len,
"created object must be removed on failure"
);
assert!(db.find_by_name("BV-B").is_some());
}
#[test]
fn create_object_rename_then_later_failure_removes_renamed_object() {
let mut db = ObjectDatabase::new();
let before_len = db.len();
let mut name_buf = BytesMut::new();
bacnet_encoding::primitives::encode_app_character_string(&mut name_buf, "Fresh-Name").unwrap();
let mut bad_type_buf = BytesMut::new();
bacnet_encoding::primitives::encode_app_enumerated(&mut bad_type_buf, 5);
let req = CreateObjectRequest {
object_specifier: ObjectSpecifier::Type(ObjectType::BINARY_VALUE),
list_of_initial_values: vec![
bacnet_services::common::BACnetPropertyValue {
property_identifier: PropertyIdentifier::OBJECT_NAME,
property_array_index: None,
value: name_buf.to_vec(),
priority: None,
},
bacnet_services::common::BACnetPropertyValue {
property_identifier: PropertyIdentifier::OBJECT_TYPE,
property_array_index: None,
value: bad_type_buf.to_vec(),
priority: None,
},
],
};
let mut buf = BytesMut::new();
req.encode(&mut buf);
assert!(
handle_create_object(&mut db, &buf, &mut BytesMut::new()).is_err(),
"read-only initial value must fail the create"
);
assert_eq!(
db.len(),
before_len,
"created object must be removed on failure"
);
assert!(
db.find_by_name("Fresh-Name").is_none(),
"remove must free the current (renamed) name"
);
db.add(Box::new(BinaryValueObject::new(9, "Fresh-Name").unwrap()))
.unwrap();
assert!(db.find_by_name("Fresh-Name").is_some());
}