use super::*;
use crate::mutation::MutationTarget;
use bacnet_services::wpm::WritePropertyMultipleError;
use bacnet_types::error::RemoteError;
async fn completed(fixture: &Fixture) {
tokio::time::timeout(Duration::from_secs(2), async {
while fixture.server.request_tasks.counters().confirmed_active != 0
|| !fixture
.server
.notification_transactions
.delivery_workers_idle()
{
tokio::task::yield_now().await;
}
})
.await
.expect("request and Audit delivery owners must finish");
}
#[tokio::test]
async fn indexed_absence_served_wpm_audits_only_prefix_and_preserves_source_writes() {
let mut fixture = server(reporter()).await;
let target = oid(ObjectType::ANALOG_INPUT, 1);
let known_source = oid(ObjectType::DEVICE, 30);
fixture
.server
.device_bindings
.write()
.await
.insert_configured(DeviceBinding::local(known_source, SOURCE).unwrap(), |_| {
false
})
.unwrap();
let absent = PropertyIdentifier::from_raw(5555);
for input in [vec![0x21, 1], vec![0x41, 0]] {
let mut bytes = BytesMut::new();
WritePropertyRequest {
object_identifier: target,
property_identifier: absent,
property_array_index: Some(1),
property_value: input,
priority: None,
}
.encode(&mut bytes)
.unwrap();
let Apdu::Error(error) = dispatch(
&fixture.server,
ConfirmedServiceChoice::WRITE_PROPERTY,
bytes.freeze(),
)
.await
else {
panic!("expected WP error");
};
assert_eq!(
(error.error_class, error.error_code),
(ErrorClass::PROPERTY, ErrorCode::UNKNOWN_PROPERTY)
);
completed(&fixture).await;
assert!(notifications(&fixture.transport.sent).is_empty());
}
let calls = Arc::new(StdMutex::new(Vec::new()));
let seen = calls.clone();
fixture.server.config_mut().policy =
Some(Arc::new(crate::FnPolicy::new().mutation(move |context| {
let MutationTarget::WritePropertyMultiple(attempt) = &context.target else {
panic!("expected WPM");
};
seen.lock().unwrap().push(attempt.clone());
true
})));
let prefix = vec![0x75, 7, 0, b'p', b'r', b'e', b'f', b'i', b'x'];
let mut bytes = BytesMut::new();
WritePropertyMultipleRequest {
list_of_write_access_specs: vec![WriteAccessSpecification {
object_identifier: target,
list_of_properties: vec![
BACnetPropertyValue {
property_identifier: PropertyIdentifier::DESCRIPTION,
property_array_index: None,
value: prefix.clone(),
priority: None,
},
BACnetPropertyValue {
property_identifier: absent,
property_array_index: Some(1),
value: vec![0x41, 0],
priority: None,
},
BACnetPropertyValue {
property_identifier: PropertyIdentifier::DESCRIPTION,
property_array_index: None,
value: vec![0x72, 0, b'x'],
priority: None,
},
],
}],
}
.encode(&mut bytes)
.unwrap();
let Apdu::Error(error) = dispatch(
&fixture.server,
ConfirmedServiceChoice::WRITE_PROPERTY_MULTIPLE,
bytes.freeze(),
)
.await
else {
panic!("expected formal WPM failure");
};
let detail = WritePropertyMultipleError::from_error_pdu(&error).unwrap();
assert_eq!(
(detail.error_class, detail.error_code),
(ErrorClass::PROPERTY, ErrorCode::UNKNOWN_PROPERTY)
);
assert_eq!(detail.first_failed_write_attempt.object_identifier, target);
assert_eq!(
detail.first_failed_write_attempt.property_identifier,
absent.to_raw()
);
assert_eq!(
detail.first_failed_write_attempt.property_array_index,
Some(1)
);
completed(&fixture).await;
{
let calls = calls.lock().unwrap();
assert_eq!(calls.len(), 1);
assert_eq!(calls[0].reference.object_identifier, target);
assert_eq!(
calls[0].reference.property_identifier,
PropertyIdentifier::DESCRIPTION.to_raw()
);
assert_eq!(calls[0].reference.property_array_index, None);
assert_eq!(calls[0].value, prefix);
}
assert_eq!(
fixture
.server
.db
.read()
.await
.get(&target)
.unwrap()
.read_property(PropertyIdentifier::DESCRIPTION, None)
.unwrap(),
PropertyValue::CharacterString("prefix".into())
);
let emitted = notifications(&fixture.transport.sent);
assert_eq!(emitted.len(), 1);
assert_eq!(emitted[0].notifications.len(), 1);
let record = &emitted[0].notifications[0];
assert_eq!(record.target_object, Some(target));
assert_eq!(
record.target_property,
Some(AuditPropertyReference {
property_identifier: PropertyIdentifier::DESCRIPTION,
property_array_index: None
})
);
assert_eq!(record.target_value, Some(prefix));
assert_eq!(record.invoke_id, Some(error.invoke_id));
assert_eq!(record.source_device, BACnetRecipient::Device(known_source));
assert_eq!(record.result, None);
assert_eq!(fixture.attempts.load(Ordering::Acquire), 0);
fixture.server.config_mut().policy = None;
let controlled = oid(ObjectType::BINARY_VALUE, 1);
assert!(matches!(
dispatch(
&fixture.server,
ConfirmedServiceChoice::WRITE_PROPERTY,
wp(
controlled,
PropertyIdentifier::PRESENT_VALUE,
vec![0x91, 1],
None
)
)
.await,
Apdu::SimpleAck(_)
));
completed(&fixture).await;
assert_eq!(fixture.attempts.load(Ordering::Acquire), 1);
assert_eq!(fixture.writes.load(Ordering::Acquire), 1);
let emitted = notifications(&fixture.transport.sent);
assert_eq!(emitted.len(), 2);
assert_eq!(emitted[1].notifications.len(), 1);
assert_eq!(emitted[1].notifications[0].target_object, Some(controlled));
assert_eq!(
emitted[1].notifications[0].source_device,
BACnetRecipient::Device(known_source)
);
assert_eq!(
emitted[1].notifications[0].target_value,
Some(vec![0x91, 1])
);
assert_eq!(emitted[1].notifications[0].result, None);
fixture.server.stop().await.unwrap();
}
#[tokio::test]
async fn local_index_refusal_reaches_no_audit_record() {
let mut fixture = server(reporter()).await;
let target = oid(ObjectType::ANALOG_INPUT, 1);
let write = |index| {
fixture.server.write_local(
&target,
PropertyIdentifier::DESCRIPTION,
index,
PropertyValue::CharacterString("local".into()),
None,
crate::LocalCommandSource::ServerDevice,
)
};
let error = write(Some(1)).await.unwrap_err();
assert!(
matches!(error, Error::Remote(RemoteError::Error { class, code })
if class == ErrorClass::PROPERTY
&& code == ErrorCode::PROPERTY_IS_NOT_AN_ARRAY),
"{error:?}"
);
completed(&fixture).await;
settle().await;
assert!(notifications(&fixture.transport.sent).is_empty());
write(None).await.unwrap();
completed(&fixture).await;
settle().await;
let emitted = notifications(&fixture.transport.sent);
assert_eq!(emitted.len(), 1);
assert_eq!(emitted[0].notifications[0].target_object, Some(target));
fixture.server.stop().await.unwrap();
}
#[path = "audit_reporter_wpm_ahead_tests.rs"]
mod wpm_ahead;