use bacnet_types::error::RemoteError;
use std::borrow::Cow;
use std::sync::atomic::{AtomicUsize, Ordering};
use std::sync::{Arc, Mutex};
use bacnet_objects::audit::{
AuditLogObject, AuditLogPersistence, AuditLogQueryPage, AuditLogSnapshot, AuditLogStorage,
};
use bacnet_services::audit::{AuditLogQueryRequest, BACnetAuditLogQueryParameters};
use bacnet_types::constructed::{
BACnetAuditLogDatum, BACnetAuditLogRecord, BACnetAuditNotification, BACnetRecipient,
};
use bacnet_types::enums::{AuditOperation, BACnetSuccessFilter, ErrorClass, ErrorCode};
use bacnet_types::primitives::{Date, Time};
use super::*;
#[derive(Default)]
struct MemoryPersistence(Mutex<Option<AuditLogSnapshot>>);
impl AuditLogPersistence for MemoryPersistence {
fn load(&self, _expected_object: ObjectIdentifier) -> Result<Option<AuditLogSnapshot>, Error> {
Ok(self.0.lock().unwrap().clone())
}
fn commit(&self, snapshot: &AuditLogSnapshot) -> Result<(), Error> {
*self.0.lock().unwrap() = Some(snapshot.clone());
Ok(())
}
}
fn oid(object_type: ObjectType, instance: u32) -> ObjectIdentifier {
ObjectIdentifier::new(object_type, instance).unwrap()
}
fn audit_record(target: ObjectIdentifier) -> BACnetAuditLogRecord {
let source = oid(ObjectType::DEVICE, 1);
BACnetAuditLogRecord {
timestamp: (
Date {
year: 124,
month: 2,
day: 29,
day_of_week: 4,
},
Time {
hour: 12,
minute: 0,
second: 0,
hundredths: 0,
},
),
datum: BACnetAuditLogDatum::AuditNotification(BACnetAuditNotification {
source_timestamp: None,
target_timestamp: None,
source_device: BACnetRecipient::Device(source),
source_object: None,
operation: AuditOperation::READ,
source_comment: None,
target_comment: None,
invoke_id: None,
source_user_id: None,
source_user_role: None,
target_device: BACnetRecipient::Device(target),
target_object: None,
target_property: None,
target_priority: None,
target_value: None,
current_value: None,
result: None,
}),
}
}
fn failure_record(target: ObjectIdentifier) -> BACnetAuditLogRecord {
let mut record = audit_record(target);
let BACnetAuditLogDatum::AuditNotification(notification) = &mut record.datum else {
unreachable!("audit helper must build a notification");
};
notification.result = Some((ErrorClass::SERVICES, ErrorCode::SERVICE_REQUEST_DENIED));
record
}
fn request(audit_log: ObjectIdentifier) -> AuditLogQueryRequest {
AuditLogQueryRequest {
audit_log,
query_parameters: BACnetAuditLogQueryParameters::BySource {
source_device_identifier: oid(ObjectType::DEVICE, 1),
source_device_address: None,
source_object_identifier: None,
operations: None,
successful_actions_only: BACnetSuccessFilter::ALL,
},
start_at_sequence_number: None,
requested_count: 10,
}
}
fn encode_request(request: &AuditLogQueryRequest) -> Vec<u8> {
let mut encoded = BytesMut::new();
request.try_encode(&mut encoded).unwrap();
encoded.to_vec()
}
fn assert_protocol(error: Error, class: ErrorClass, code: ErrorCode) {
assert!(matches!(
error,
Error::Remote(RemoteError::Error { class: actual_class, code: actual_code })
if actual_class == class && actual_code == code
));
}
#[test]
fn handler_decodes_then_returns_an_owned_page_from_real_audit_storage() {
let audit_oid = oid(ObjectType::AUDIT_LOG, 7);
let target = oid(ObjectType::DEVICE, 2);
let mut log =
AuditLogObject::new(7, "Audit-7", 4, Arc::new(MemoryPersistence::default())).unwrap();
log.add_record(audit_record(target)).unwrap();
let mut db = ObjectDatabase::new();
db.add(Box::new(log)).unwrap();
let (returned_oid, page) =
handle_audit_log_query(&db, &encode_request(&request(audit_oid))).unwrap();
assert_eq!(returned_oid, audit_oid);
assert_eq!(page.records.len(), 1);
assert_eq!(page.records[0].sequence_number, 1);
assert!(page.no_more_items);
}
#[test]
fn handler_applies_three_state_filter_and_u64_cursor_over_real_storage() {
let audit_oid = oid(ObjectType::AUDIT_LOG, 7);
let target = oid(ObjectType::DEVICE, 2);
let mut log =
AuditLogObject::new(7, "Audit-7", 4, Arc::new(MemoryPersistence::default())).unwrap();
log.add_record(audit_record(target)).unwrap();
log.add_record(failure_record(target)).unwrap();
let mut db = ObjectDatabase::new();
db.add(Box::new(log)).unwrap();
let filtered = |filter, start| {
let mut request = request(audit_oid);
request.query_parameters = BACnetAuditLogQueryParameters::BySource {
source_device_identifier: oid(ObjectType::DEVICE, 1),
source_device_address: None,
source_object_identifier: None,
operations: None,
successful_actions_only: filter,
};
request.start_at_sequence_number = start;
let (_, page) = handle_audit_log_query(&db, &encode_request(&request)).unwrap();
page.records
.iter()
.map(|entry| entry.sequence_number)
.collect::<Vec<_>>()
};
assert_eq!(filtered(BACnetSuccessFilter::SUCCESSES_ONLY, None), vec![1]);
assert_eq!(filtered(BACnetSuccessFilter::FAILURES_ONLY, None), vec![2]);
assert_eq!(filtered(BACnetSuccessFilter::ALL, None), vec![2, 1]);
assert_eq!(
filtered(BACnetSuccessFilter::ALL, Some(2)),
vec![1],
"literal cursor admits only identities below it"
);
assert_eq!(
filtered(BACnetSuccessFilter::ALL, Some(u64::from(u32::MAX) + 1)),
vec![2, 1],
"u64 cursor above u32::MAX must pass through untruncated"
);
}
#[test]
fn missing_or_non_audit_identifiers_map_to_unknown_object() {
let db = make_db_with_ai();
for requested in [
oid(ObjectType::AUDIT_LOG, 99),
oid(ObjectType::ANALOG_INPUT, 1),
] {
let error = handle_audit_log_query(&db, &encode_request(&request(requested))).unwrap_err();
assert_protocol(error, ErrorClass::OBJECT, ErrorCode::UNKNOWN_OBJECT);
}
}
struct AuditWithoutCapability {
oid: ObjectIdentifier,
}
impl BACnetObject for AuditWithoutCapability {
fn object_identifier(&self) -> ObjectIdentifier {
self.oid
}
fn object_name(&self) -> &str {
"AuditWithoutCapability"
}
fn read_property(
&self,
_property: PropertyIdentifier,
_array_index: Option<u32>,
) -> Result<PropertyValue, Error> {
Err(Error::protocol(
ErrorClass::PROPERTY,
ErrorCode::UNKNOWN_PROPERTY,
))
}
fn write_property(
&mut self,
_property: PropertyIdentifier,
_array_index: Option<u32>,
_value: PropertyValue,
_priority: Option<u8>,
) -> Result<(), Error> {
Err(Error::protocol(
ErrorClass::PROPERTY,
ErrorCode::WRITE_ACCESS_DENIED,
))
}
fn property_list(&self) -> Cow<'static, [PropertyIdentifier]> {
Cow::Borrowed(&[])
}
}
#[test]
fn audit_typed_object_without_capability_maps_to_optional_functionality() {
let audit_oid = oid(ObjectType::AUDIT_LOG, 8);
let mut db = ObjectDatabase::new();
db.add(Box::new(AuditWithoutCapability { oid: audit_oid }))
.unwrap();
let error = handle_audit_log_query(&db, &encode_request(&request(audit_oid))).unwrap_err();
assert_protocol(
error,
ErrorClass::SERVICES,
ErrorCode::OPTIONAL_FUNCTIONALITY_NOT_SUPPORTED,
);
}
struct SpyAuditStorage {
query_count: AtomicUsize,
}
impl AuditLogStorage for SpyAuditStorage {
fn query(
&self,
_parameters: &BACnetAuditLogQueryParameters,
_start_at_sequence_number: Option<u64>,
_requested_count: u16,
) -> AuditLogQueryPage {
self.query_count.fetch_add(1, Ordering::SeqCst);
AuditLogQueryPage {
records: Vec::new(),
no_more_items: true,
}
}
fn retained_records(
&self,
) -> &std::collections::VecDeque<bacnet_types::constructed::BACnetAuditLogRecordResult> {
static EMPTY: std::collections::VecDeque<
bacnet_types::constructed::BACnetAuditLogRecordResult,
> = std::collections::VecDeque::new();
&EMPTY
}
}
struct SpyAuditObject {
oid: ObjectIdentifier,
storage: Arc<SpyAuditStorage>,
}
impl BACnetObject for SpyAuditObject {
fn object_identifier(&self) -> ObjectIdentifier {
self.oid
}
fn object_name(&self) -> &str {
"SpyAuditObject"
}
fn read_property(
&self,
_property: PropertyIdentifier,
_array_index: Option<u32>,
) -> Result<PropertyValue, Error> {
unreachable!("AuditLogQuery must not inspect properties")
}
fn write_property(
&mut self,
_property: PropertyIdentifier,
_array_index: Option<u32>,
_value: PropertyValue,
_priority: Option<u8>,
) -> Result<(), Error> {
unreachable!("AuditLogQuery is read-only")
}
fn property_list(&self) -> Cow<'static, [PropertyIdentifier]> {
Cow::Borrowed(&[])
}
fn audit_log_storage_internal(&self) -> Option<&dyn AuditLogStorage> {
Some(self.storage.as_ref())
}
}
#[test]
fn malformed_payload_fails_before_audit_storage_is_inspected() {
let audit_oid = oid(ObjectType::AUDIT_LOG, 9);
let storage = Arc::new(SpyAuditStorage {
query_count: AtomicUsize::new(0),
});
let mut db = ObjectDatabase::new();
db.add(Box::new(SpyAuditObject {
oid: audit_oid,
storage: Arc::clone(&storage),
}))
.unwrap();
let mut malformed = encode_request(&request(audit_oid));
malformed.push(0);
assert!(handle_audit_log_query(&db, &malformed).is_err());
assert_eq!(storage.query_count.load(Ordering::SeqCst), 0);
}