1use std::borrow::Cow;
4use std::collections::VecDeque;
5use std::sync::Arc;
6
7use bacnet_types::bitstring::{AuditOperationFlags, BACnetPriorityFilter};
8use bacnet_types::constructed::{
9 BACnetAuditLogDatum, BACnetAuditLogQueryParameters, BACnetAuditLogRecord,
10 BACnetAuditLogRecordResult, BACnetAuditNotification, BACnetRecipient,
11};
12use bacnet_types::enums::{
13 AuditLevel, ErrorClass, ErrorCode, EventState, ObjectType, PropertyIdentifier, Reliability,
14};
15use bacnet_types::error::Error;
16use bacnet_types::primitives::{ObjectIdentifier, PropertyValue, StatusFlags};
17
18use crate::clock::ClockReader;
19use crate::common::read_property_list_property;
20use crate::property_metadata::PropertyMetadata;
21use crate::traits::{BACnetObject, WritePropertyRollback};
22
23mod notification;
24mod persistence;
25mod receipt;
26mod reporter_metadata;
27pub use notification::AuditLogNotificationSink;
28use persistence::{validate_record, validate_snapshot};
29pub use persistence::{
30 AuditLogPersistence, AuditLogSnapshot, FileAuditLogPersistence, MAX_AUDIT_RECORDS,
31};
32pub use receipt::{
33 CompletedAuditReceipt, ConfirmedAuditNotificationOutcome, MAX_AUDIT_RECEIPT_KEY_BYTES,
34 MAX_COMPLETED_AUDIT_RECEIPTS,
35};
36
37#[derive(Debug, Clone, PartialEq)]
39pub struct AuditLogQueryPage {
40 pub records: Vec<BACnetAuditLogRecordResult>,
42 pub no_more_items: bool,
44}
45
46pub trait AuditLogStorage: Send + Sync {
52 fn query(
58 &self,
59 parameters: &BACnetAuditLogQueryParameters,
60 start_at_sequence_number: Option<u32>,
61 requested_count: u16,
62 ) -> AuditLogQueryPage;
63}
64
65pub struct AuditLogObject {
71 oid: ObjectIdentifier,
72 name: String,
73 description: String,
74 log_enable: bool,
75 buffer_size: u32,
76 buffer: VecDeque<BACnetAuditLogRecordResult>,
77 completed_receipts: Vec<CompletedAuditReceipt>,
78 total_record_count: u64,
79 status_flags: StatusFlags,
80 generation: u64,
81 persistence: Arc<dyn AuditLogPersistence>,
82 clock: Option<Arc<dyn ClockReader>>,
83}
84
85struct AuditLogWriteRollback {
86 snapshot: AuditLogSnapshot,
87}
88
89const LOG_DISABLED_STATUS: u8 = 0b001;
90const BUFFER_PURGED_STATUS: u8 = 0b010;
91
92impl AuditLogObject {
93 pub fn new(
95 instance: u32,
96 name: impl Into<String>,
97 buffer_size: u32,
98 persistence: Arc<dyn AuditLogPersistence>,
99 ) -> Result<Self, Error> {
100 let oid = ObjectIdentifier::new(ObjectType::AUDIT_LOG, instance)?;
101 if buffer_size > MAX_AUDIT_RECORDS {
102 return Err(Error::OutOfRange(format!(
103 "AuditLog capacity {buffer_size} exceeds {MAX_AUDIT_RECORDS}"
104 )));
105 }
106 let snapshot = match persistence.load(oid)? {
107 Some(snapshot) => {
108 if snapshot.object_identifier != oid || snapshot.capacity != buffer_size {
109 return Err(Error::Encoding(
110 "AuditLog persisted identity or capacity does not match configuration"
111 .into(),
112 ));
113 }
114 validate_snapshot(&snapshot)?;
115 snapshot
116 }
117 None => {
118 let snapshot = AuditLogSnapshot {
119 object_identifier: oid,
120 generation: 1,
121 capacity: buffer_size,
122 log_enable: true,
123 total_record_count: 0,
124 records: Vec::new(),
125 completed_receipts: Vec::new(),
126 };
127 validate_snapshot(&snapshot)?;
128 persistence.commit(&snapshot)?;
129 snapshot
130 }
131 };
132 Ok(Self {
133 oid,
134 name: name.into(),
135 description: String::new(),
136 log_enable: snapshot.log_enable,
137 buffer_size: snapshot.capacity,
138 buffer: snapshot.records.into(),
139 completed_receipts: snapshot.completed_receipts,
140 total_record_count: snapshot.total_record_count,
141 status_flags: StatusFlags::empty(),
142 generation: snapshot.generation,
143 persistence,
144 clock: None,
145 })
146 }
147
148 pub fn add_record(&mut self, record: BACnetAuditLogRecord) -> Result<Option<u64>, Error> {
150 if !self.log_enable {
151 return Ok(None);
152 }
153 validate_record(&record)?;
154 let mut prospective = self.snapshot_for_next_generation()?;
155 let sequence_number = append_record(&mut prospective, record);
156 self.commit_and_apply(prospective)?;
157 Ok(Some(sequence_number))
158 }
159
160 pub fn records(&self) -> &VecDeque<BACnetAuditLogRecordResult> {
162 &self.buffer
163 }
164
165 pub fn buffer_size(&self) -> u32 {
167 self.buffer_size
168 }
169
170 pub fn log_enable(&self) -> bool {
172 self.log_enable
173 }
174
175 pub fn total_record_count(&self) -> u64 {
177 self.total_record_count
178 }
179
180 pub fn generation(&self) -> u64 {
182 self.generation
183 }
184
185 fn purge(&mut self) -> Result<u64, Error> {
187 let timestamp = self.valid_timestamp()?;
188 let mut prospective = self.snapshot_for_next_generation()?;
189 prospective.records.clear();
190 let sequence_number = append_record(
191 &mut prospective,
192 BACnetAuditLogRecord {
193 timestamp,
194 datum: BACnetAuditLogDatum::LogStatus(BUFFER_PURGED_STATUS),
195 },
196 );
197 self.commit_and_apply(prospective)?;
198 Ok(sequence_number)
199 }
200
201 pub fn set_description(&mut self, desc: impl Into<String>) {
203 self.description = desc.into();
204 }
205
206 fn valid_timestamp(
207 &self,
208 ) -> Result<
209 (
210 bacnet_types::primitives::Date,
211 bacnet_types::primitives::Time,
212 ),
213 Error,
214 > {
215 let frame = self
216 .clock
217 .as_ref()
218 .and_then(|clock| clock.read_clock())
219 .filter(|frame| frame.is_valid_actual_datetime())
220 .ok_or(Error::Protocol {
221 class: ErrorClass::DEVICE.to_raw() as u32,
222 code: ErrorCode::OPERATIONAL_PROBLEM.to_raw() as u32,
223 })?;
224 Ok((frame.local_date, frame.local_time))
225 }
226
227 fn snapshot_for_next_generation(&self) -> Result<AuditLogSnapshot, Error> {
228 let generation = self
229 .generation
230 .checked_add(1)
231 .ok_or_else(|| Error::OutOfRange("AuditLog persistence generation exhausted".into()))?;
232 let mut snapshot = self.current_snapshot();
233 snapshot.generation = generation;
234 Ok(snapshot)
235 }
236
237 fn current_snapshot(&self) -> AuditLogSnapshot {
238 AuditLogSnapshot {
239 object_identifier: self.oid,
240 generation: self.generation,
241 capacity: self.buffer_size,
242 log_enable: self.log_enable,
243 total_record_count: self.total_record_count,
244 records: self.buffer.iter().cloned().collect(),
245 completed_receipts: self.completed_receipts.clone(),
246 }
247 }
248
249 fn commit_and_apply(&mut self, snapshot: AuditLogSnapshot) -> Result<(), Error> {
250 validate_snapshot(&snapshot)?;
251 self.persistence.commit(&snapshot)?;
252 self.generation = snapshot.generation;
253 self.log_enable = snapshot.log_enable;
254 self.total_record_count = snapshot.total_record_count;
255 self.buffer = snapshot.records.into();
256 self.completed_receipts = snapshot.completed_receipts;
257 Ok(())
258 }
259}
260
261fn append_record(snapshot: &mut AuditLogSnapshot, record: BACnetAuditLogRecord) -> u64 {
262 let sequence_number = if snapshot.total_record_count == u64::MAX {
263 1
264 } else {
265 snapshot.total_record_count + 1
266 };
267 snapshot.total_record_count = sequence_number;
268 if snapshot.capacity != 0 {
269 if snapshot.records.len() >= snapshot.capacity as usize {
270 snapshot.records.remove(0);
271 }
272 snapshot.records.push(BACnetAuditLogRecordResult {
273 sequence_number,
274 record,
275 });
276 }
277 sequence_number
278}
279
280fn recipient_matches(
281 actual: &BACnetRecipient,
282 required_identifier: ObjectIdentifier,
283 optional_address: Option<&bacnet_types::constructed::BACnetAddress>,
284) -> bool {
285 match actual {
286 BACnetRecipient::Device(identifier) => *identifier == required_identifier,
287 BACnetRecipient::Address(address) => {
288 optional_address.is_some_and(|filter| address == filter)
289 }
290 }
291}
292
293fn operation_matches(
294 notification: &BACnetAuditNotification,
295 operations: Option<bacnet_types::bitstring::AuditOperationFlags>,
296 successful_actions_only: bool,
297) -> bool {
298 operations.is_none_or(|flags| flags.contains(notification.operation))
299 && (!successful_actions_only || notification.result.is_none())
300}
301
302fn query_matches(
303 notification: &BACnetAuditNotification,
304 parameters: &BACnetAuditLogQueryParameters,
305) -> bool {
306 match parameters {
307 BACnetAuditLogQueryParameters::ByTarget {
308 target_device_identifier,
309 target_device_address,
310 target_object_identifier,
311 target_property_identifier,
312 target_array_index,
313 target_priority,
314 operations,
315 successful_actions_only,
316 } => {
317 recipient_matches(
318 ¬ification.target_device,
319 *target_device_identifier,
320 target_device_address.as_ref(),
321 ) && target_object_identifier
322 .is_none_or(|filter| notification.target_object == Some(filter))
323 && target_property_identifier.is_none_or(|filter| {
324 notification.target_property.as_ref().is_some_and(|property| {
325 property.property_identifier == filter
326 })
327 })
328 && target_array_index.is_none_or(|filter| {
329 notification.target_property.as_ref().is_some_and(|property| {
330 property.property_array_index == Some(filter)
331 })
332 })
333 && target_priority.is_none_or(|filter| {
336 notification
337 .target_priority
338 .is_none_or(|priority| priority == filter)
339 })
340 && operation_matches(notification, *operations, *successful_actions_only)
341 }
342 BACnetAuditLogQueryParameters::BySource {
343 source_device_identifier,
344 source_device_address,
345 source_object_identifier,
346 operations,
347 successful_actions_only,
348 } => {
349 recipient_matches(
350 ¬ification.source_device,
351 *source_device_identifier,
352 source_device_address.as_ref(),
353 ) && source_object_identifier
354 .is_none_or(|filter| notification.source_object == Some(filter))
355 && operation_matches(notification, *operations, *successful_actions_only)
356 }
357 }
358}
359
360impl AuditLogStorage for AuditLogObject {
361 fn query(
362 &self,
363 parameters: &BACnetAuditLogQueryParameters,
364 start_at_sequence_number: Option<u32>,
365 requested_count: u16,
366 ) -> AuditLogQueryPage {
367 let limit = usize::from(requested_count).min(MAX_AUDIT_RECORDS as usize);
368 let mut records = Vec::with_capacity(limit.min(self.buffer.len()));
369 let mut unreturned_match = false;
370
371 for result in self.buffer.iter().rev() {
375 if start_at_sequence_number
376 .is_some_and(|start| result.sequence_number >= u64::from(start))
377 {
378 continue;
379 }
380 let BACnetAuditLogDatum::AuditNotification(notification) = &result.record.datum else {
381 continue;
382 };
383 if !query_matches(notification, parameters) {
384 continue;
385 }
386 if records.len() < limit {
387 records.push(result.clone());
388 } else {
389 unreturned_match = true;
393 }
394 }
395
396 AuditLogQueryPage {
397 records,
398 no_more_items: !unreturned_match,
399 }
400 }
401}
402
403impl BACnetObject for AuditLogObject {
404 fn object_identifier(&self) -> ObjectIdentifier {
405 self.oid
406 }
407
408 fn object_name(&self) -> &str {
409 &self.name
410 }
411
412 fn read_property(
413 &self,
414 property: PropertyIdentifier,
415 array_index: Option<u32>,
416 ) -> Result<PropertyValue, Error> {
417 match property {
418 p if p == PropertyIdentifier::OBJECT_IDENTIFIER => {
419 Ok(PropertyValue::ObjectIdentifier(self.oid))
420 }
421 p if p == PropertyIdentifier::OBJECT_NAME => {
422 Ok(PropertyValue::CharacterString(self.name.clone()))
423 }
424 p if p == PropertyIdentifier::DESCRIPTION => {
425 Ok(PropertyValue::CharacterString(self.description.clone()))
426 }
427 p if p == PropertyIdentifier::OBJECT_TYPE => {
428 Ok(PropertyValue::Enumerated(ObjectType::AUDIT_LOG.to_raw()))
429 }
430 p if p == PropertyIdentifier::LOG_ENABLE => Ok(PropertyValue::Boolean(self.log_enable)),
431 p if p == PropertyIdentifier::BUFFER_SIZE => {
432 Ok(PropertyValue::Unsigned(self.buffer_size as u64))
433 }
434 p if p == PropertyIdentifier::RECORD_COUNT => {
435 Ok(PropertyValue::Unsigned(self.buffer.len() as u64))
436 }
437 p if p == PropertyIdentifier::TOTAL_RECORD_COUNT => {
438 Ok(PropertyValue::Unsigned(self.total_record_count))
439 }
440 p if p == PropertyIdentifier::STATUS_FLAGS => Ok(PropertyValue::BitString {
441 unused_bits: 4,
442 data: vec![self.status_flags.bits() << 4],
443 }),
444 p if p == PropertyIdentifier::EVENT_STATE => Ok(PropertyValue::Enumerated(0)),
445 p if p == PropertyIdentifier::PROPERTY_LIST => {
446 read_property_list_property(&self.property_list(), array_index)
447 }
448 _ => Err(Error::Protocol {
449 class: ErrorClass::PROPERTY.to_raw() as u32,
450 code: ErrorCode::UNKNOWN_PROPERTY.to_raw() as u32,
451 }),
452 }
453 }
454
455 fn write_property(
456 &mut self,
457 property: PropertyIdentifier,
458 _array_index: Option<u32>,
459 value: PropertyValue,
460 _priority: Option<u8>,
461 ) -> Result<(), Error> {
462 if property == PropertyIdentifier::LOG_ENABLE {
463 if let PropertyValue::Boolean(v) = value {
464 if v == self.log_enable {
465 return Ok(());
466 }
467 let timestamp = self.valid_timestamp()?;
468 let mut prospective = self.snapshot_for_next_generation()?;
469 prospective.log_enable = v;
470 append_record(
471 &mut prospective,
472 BACnetAuditLogRecord {
473 timestamp,
474 datum: BACnetAuditLogDatum::LogStatus(if v {
475 0
476 } else {
477 LOG_DISABLED_STATUS
478 }),
479 },
480 );
481 self.commit_and_apply(prospective)?;
482 return Ok(());
483 }
484 return Err(Error::Protocol {
485 class: ErrorClass::PROPERTY.to_raw() as u32,
486 code: ErrorCode::INVALID_DATA_TYPE.to_raw() as u32,
487 });
488 }
489 if property == PropertyIdentifier::RECORD_COUNT {
490 return Err(Error::Protocol {
491 class: ErrorClass::PROPERTY.to_raw() as u32,
492 code: ErrorCode::WRITE_ACCESS_DENIED.to_raw() as u32,
493 });
494 }
495 if property == PropertyIdentifier::DESCRIPTION {
496 if let PropertyValue::CharacterString(s) = value {
497 self.description = s;
498 return Ok(());
499 }
500 return Err(Error::Protocol {
501 class: ErrorClass::PROPERTY.to_raw() as u32,
502 code: ErrorCode::INVALID_DATA_TYPE.to_raw() as u32,
503 });
504 }
505 Err(Error::Protocol {
506 class: ErrorClass::PROPERTY.to_raw() as u32,
507 code: ErrorCode::WRITE_ACCESS_DENIED.to_raw() as u32,
508 })
509 }
510
511 fn property_list(&self) -> Cow<'static, [PropertyIdentifier]> {
512 static PROPS: &[PropertyIdentifier] = &[
513 PropertyIdentifier::OBJECT_IDENTIFIER,
514 PropertyIdentifier::OBJECT_NAME,
515 PropertyIdentifier::DESCRIPTION,
516 PropertyIdentifier::OBJECT_TYPE,
517 PropertyIdentifier::LOG_ENABLE,
518 PropertyIdentifier::BUFFER_SIZE,
519 PropertyIdentifier::RECORD_COUNT,
520 PropertyIdentifier::TOTAL_RECORD_COUNT,
521 PropertyIdentifier::STATUS_FLAGS,
522 PropertyIdentifier::EVENT_STATE,
523 ];
524 Cow::Borrowed(PROPS)
525 }
526
527 fn bind_clock_internal(&mut self, clock: Option<Arc<dyn ClockReader>>) {
528 self.clock = clock;
529 }
530
531 fn audit_log_storage_internal(&self) -> Option<&dyn AuditLogStorage> {
532 Some(self)
533 }
534
535 fn audit_log_notification_sink_internal(
536 &mut self,
537 ) -> Option<&mut dyn AuditLogNotificationSink> {
538 Some(self)
539 }
540
541 fn capture_write_property_rollback(
542 &mut self,
543 property: PropertyIdentifier,
544 value: &PropertyValue,
545 ) -> Option<WritePropertyRollback> {
546 let PropertyValue::Boolean(requested) = value else {
547 return None;
548 };
549 (property == PropertyIdentifier::LOG_ENABLE && *requested != self.log_enable).then(|| {
550 WritePropertyRollback::new(AuditLogWriteRollback {
551 snapshot: self.current_snapshot(),
552 })
553 })
554 }
555
556 fn restore_write_property_rollback(
557 &mut self,
558 rollback: WritePropertyRollback,
559 ) -> Result<(), Error> {
560 let mut snapshot = rollback.downcast::<AuditLogWriteRollback>()?.snapshot;
561 if snapshot.object_identifier != self.oid || snapshot.capacity != self.buffer_size {
562 return Err(Error::Encoding(
563 "AuditLog rollback snapshot does not belong to this object".into(),
564 ));
565 }
566 snapshot.generation = self
567 .generation
568 .checked_add(1)
569 .ok_or_else(|| Error::OutOfRange("AuditLog persistence generation exhausted".into()))?;
570 self.commit_and_apply(snapshot)
571 }
572}
573
574pub struct AuditReporterObject {
580 oid: ObjectIdentifier,
581 name: String,
582 description: String,
583 status_flags: StatusFlags,
584 audit_level: AuditLevel,
585 auditable_operations: AuditOperationFlags,
586 audit_priority_filter: BACnetPriorityFilter,
587 issue_confirmed_notifications: bool,
588}
589
590impl AuditReporterObject {
591 pub fn new(instance: u32, name: impl Into<String>) -> Result<Self, Error> {
592 let oid = ObjectIdentifier::new(ObjectType::AUDIT_REPORTER, instance)?;
593 Ok(Self {
594 oid,
595 name: name.into(),
596 description: String::new(),
597 status_flags: StatusFlags::empty(),
598 audit_level: AuditLevel::NONE,
599 auditable_operations: AuditOperationFlags::empty(),
600 audit_priority_filter: BACnetPriorityFilter::all(),
601 issue_confirmed_notifications: false,
602 })
603 }
604
605 pub fn set_description(&mut self, desc: impl Into<String>) {
607 self.description = desc.into();
608 }
609
610 pub fn set_audit_level(&mut self, level: AuditLevel) -> Result<(), Error> {
612 if level == AuditLevel::DEFAULT {
613 return Err(Error::OutOfRange(
614 "Audit Reporter audit level must not be DEFAULT".into(),
615 ));
616 }
617 self.audit_level = level;
618 Ok(())
619 }
620
621 pub fn set_auditable_operations(&mut self, operations: AuditOperationFlags) {
623 self.auditable_operations = operations;
624 }
625
626 pub fn set_audit_priority_filter(&mut self, filter: BACnetPriorityFilter) {
628 self.audit_priority_filter = filter;
629 }
630
631 pub fn set_issue_confirmed_notifications(&mut self, confirmed: bool) {
633 self.issue_confirmed_notifications = confirmed;
634 }
635}
636
637impl BACnetObject for AuditReporterObject {
638 fn object_identifier(&self) -> ObjectIdentifier {
639 self.oid
640 }
641
642 fn object_name(&self) -> &str {
643 &self.name
644 }
645
646 fn property_metadata(&self) -> Cow<'_, [PropertyMetadata]> {
647 Cow::Borrowed(reporter_metadata::AUDIT_REPORTER_PROPERTIES)
648 }
649
650 fn read_property(
651 &self,
652 property: PropertyIdentifier,
653 array_index: Option<u32>,
654 ) -> Result<PropertyValue, Error> {
655 match property {
656 p if p == PropertyIdentifier::OBJECT_IDENTIFIER => {
657 Ok(PropertyValue::ObjectIdentifier(self.oid))
658 }
659 p if p == PropertyIdentifier::OBJECT_NAME => {
660 Ok(PropertyValue::CharacterString(self.name.clone()))
661 }
662 p if p == PropertyIdentifier::DESCRIPTION => {
663 Ok(PropertyValue::CharacterString(self.description.clone()))
664 }
665 p if p == PropertyIdentifier::OBJECT_TYPE => Ok(PropertyValue::Enumerated(
666 ObjectType::AUDIT_REPORTER.to_raw(),
667 )),
668 p if p == PropertyIdentifier::STATUS_FLAGS => Ok(PropertyValue::BitString {
669 unused_bits: 4,
670 data: vec![self.status_flags.bits() << 4],
671 }),
672 p if p == PropertyIdentifier::RELIABILITY => Ok(PropertyValue::Enumerated(
673 Reliability::NO_FAULT_DETECTED.to_raw(),
674 )),
675 p if p == PropertyIdentifier::EVENT_STATE => {
676 Ok(PropertyValue::Enumerated(EventState::NORMAL.to_raw()))
677 }
678 p if p == PropertyIdentifier::AUDIT_LEVEL => {
679 Ok(PropertyValue::Enumerated(self.audit_level.to_raw()))
680 }
681 p if p == PropertyIdentifier::AUDIT_SOURCE_REPORTER => {
682 Ok(PropertyValue::Boolean(false))
683 }
684 p if p == PropertyIdentifier::AUDITABLE_OPERATIONS => {
685 let (unused_bits, data) = self.auditable_operations.to_bacnet();
686 Ok(PropertyValue::BitString { unused_bits, data })
687 }
688 p if p == PropertyIdentifier::AUDIT_PRIORITY_FILTER => {
689 let (unused_bits, data) = self.audit_priority_filter.to_bacnet();
690 Ok(PropertyValue::BitString { unused_bits, data })
691 }
692 p if p == PropertyIdentifier::ISSUE_CONFIRMED_NOTIFICATIONS => {
693 Ok(PropertyValue::Boolean(self.issue_confirmed_notifications))
694 }
695 p if p == PropertyIdentifier::PROPERTY_LIST => {
696 read_property_list_property(&self.property_list(), array_index)
697 }
698 _ => Err(Error::Protocol {
699 class: ErrorClass::PROPERTY.to_raw() as u32,
700 code: ErrorCode::UNKNOWN_PROPERTY.to_raw() as u32,
701 }),
702 }
703 }
704
705 fn write_property(
706 &mut self,
707 property: PropertyIdentifier,
708 _array_index: Option<u32>,
709 value: PropertyValue,
710 _priority: Option<u8>,
711 ) -> Result<(), Error> {
712 if property == PropertyIdentifier::DESCRIPTION {
713 if let PropertyValue::CharacterString(s) = value {
714 self.description = s;
715 return Ok(());
716 }
717 return Err(Error::Protocol {
718 class: ErrorClass::PROPERTY.to_raw() as u32,
719 code: ErrorCode::INVALID_DATA_TYPE.to_raw() as u32,
720 });
721 }
722 Err(Error::Protocol {
723 class: ErrorClass::PROPERTY.to_raw() as u32,
724 code: ErrorCode::WRITE_ACCESS_DENIED.to_raw() as u32,
725 })
726 }
727
728 fn property_list(&self) -> Cow<'static, [PropertyIdentifier]> {
729 crate::property_metadata::property_list_from_metadata(
730 reporter_metadata::AUDIT_REPORTER_PROPERTIES,
731 )
732 }
733}
734
735#[cfg(test)]
736#[path = "audit/tests.rs"]
737mod tests;
738
739#[cfg(test)]
740#[path = "audit/query_tests.rs"]
741mod query_tests;
742
743#[cfg(test)]
744#[path = "audit/notification_tests.rs"]
745mod notification_tests;
746
747#[cfg(test)]
748#[path = "audit/receipt_tests.rs"]
749mod receipt_tests;
750
751#[cfg(test)]
752#[path = "audit/persistence_receipt_tests.rs"]
753mod persistence_receipt_tests;