bacnet-server 0.15.0

BACnet server: APDU dispatch and service handlers
Documentation
use super::*;
use bacnet_services::audit::{AuditLogQueryAck, AuditLogQueryRequest};
use bacnet_types::{constructed::BACnetAuditLogQueryParameters, enums::BACnetSuccessFilter};

#[path = "audit_reporter_query_support.rs"]
mod support;
use support::*;
#[path = "audit_reporter_query_boundary_tests.rs"]
mod boundary;

#[tokio::test]
async fn audit_reporter_query_empty_nonempty_zero_and_pages_are_value_free_and_unchanged() {
    for count in [0, 3] {
        let mut fixture = server(read_reporter()).await;
        let mut plain = plain_server(read_reporter()).await;
        let (reads, persistence) = add_log(&fixture, count, "real").await;
        let (plain_reads, _) = add_log(&plain, count, "real").await;
        let snapshot = persistence.0.lock().unwrap().clone();
        let mut expected_records = Vec::new();
        for (i, (start, requested, sequences, exhausted)) in [
            (None, 10, vec![3, 2, 1], true),
            (None, 0, vec![], false),
            (Some(u64::from(u32::MAX) + 1), 1, vec![3], false),
            (Some(3), 1, vec![2], false),
            (Some(2), 1, vec![1], true),
            (Some(1), 1, vec![], true),
        ]
        .into_iter()
        .enumerate()
        {
            let data = encode(&query(start, requested));
            let response = dispatch(&fixture.server, SERVICE, data.clone()).await;
            let baseline = dispatch(&plain.server, SERVICE, data).await;
            assert_eq!(wire(&response), wire(&baseline));
            let Apdu::ComplexAck(ack) = response else {
                panic!("{response:?}")
            };
            assert_eq!(
                AuditLogQueryAck::decode(&ack.service_ack).unwrap(),
                AuditLogQueryAck {
                    audit_log: target(),
                    records: if count == 0 {
                        vec![]
                    } else {
                        sequences
                            .into_iter()
                            .map(|s| stored_record(s, false))
                            .collect()
                    },
                    no_more_items: count == 0 || exhausted,
                }
            );
            settle().await;
            expected_records.push(expected_query(target(), 77 + i as u8, i as u16, None));
            assert_eq!(records(&fixture), expected_records);
            assert!(records(&plain).is_empty());
            assert_eq!(
                reads.load(Ordering::Acquire),
                i + 1,
                "no reread or synchronous recursion"
            );
            assert_eq!(plain_reads.load(Ordering::Acquire), i + 1);
            assert_eq!(*persistence.0.lock().unwrap(), snapshot, "no log mutation");
            assert_idle(&fixture);
        }
        fixture.server.stop().await.unwrap();
        plain.server.stop().await.unwrap();
    }
}

#[tokio::test]
async fn audit_reporter_query_filters_remain_query_only_not_notification_fields() {
    let mut fixture = server(read_reporter()).await;
    let mut plain = plain_server(read_reporter()).await;
    add_log(&fixture, 3, "real").await;
    add_log(&plain, 3, "real").await;
    for (i, (filter, sequences)) in [
        (BACnetSuccessFilter::ALL, vec![3, 2, 1]),
        (BACnetSuccessFilter::SUCCESSES_ONLY, vec![3, 1]),
        (BACnetSuccessFilter::FAILURES_ONLY, vec![2]),
    ]
    .into_iter()
    .enumerate()
    {
        let mut query = query(Some(u64::MAX), 10);
        query.query_parameters = BACnetAuditLogQueryParameters::ByTarget {
            target_device_identifier: oid(ObjectType::DEVICE, 10),
            target_device_address: None,
            target_object_identifier: Some(target()),
            target_property_identifier: Some(PropertyIdentifier::LOG_BUFFER),
            target_array_index: Some(7),
            target_priority: Some(5),
            operations: None,
            successful_actions_only: filter,
        };
        let data = encode(&query);
        let response = dispatch(&fixture.server, SERVICE, data.clone()).await;
        assert_eq!(
            wire(&response),
            wire(&dispatch(&plain.server, SERVICE, data).await)
        );
        let Apdu::ComplexAck(ack) = response else {
            panic!("{response:?}")
        };
        let ack = AuditLogQueryAck::decode(&ack.service_ack).unwrap();
        assert_eq!(
            ack.records,
            sequences
                .into_iter()
                .map(|s| stored_record(s, false))
                .collect::<Vec<_>>()
        );
        assert!(ack.no_more_items);
        settle().await;
        assert_eq!(records(&fixture).len(), i + 1);
        assert_eq!(
            records(&fixture)[i],
            expected_query(target(), 77 + i as u8, i as u16, None)
        );
    }
    fixture.server.stop().await.unwrap();
    plain.server.stop().await.unwrap();
}

#[tokio::test]
async fn audit_reporter_query_execution_errors_have_exact_result_and_response_parity() {
    for (target, mode, result) in [
        (
            oid(ObjectType::AUDIT_LOG, 99),
            "real",
            (ErrorClass::OBJECT, ErrorCode::UNKNOWN_OBJECT),
        ),
        (
            oid(ObjectType::ANALOG_INPUT, 1),
            "real",
            (ErrorClass::OBJECT, ErrorCode::UNKNOWN_OBJECT),
        ),
        (
            target(),
            "no capability",
            (
                ErrorClass::SERVICES,
                ErrorCode::OPTIONAL_FUNCTIONALITY_NOT_SUPPORTED,
            ),
        ),
    ] {
        let mut fixture = server(read_reporter()).await;
        let mut plain = plain_server(read_reporter()).await;
        let (reads, _) = add_log(&fixture, 0, mode).await;
        add_log(&plain, 0, mode).await;
        let mut query = query(None, 1);
        query.audit_log = target;
        let data = encode(&query);
        let response = dispatch(&fixture.server, SERVICE, data.clone()).await;
        assert_eq!(
            wire(&response),
            wire(&dispatch(&plain.server, SERVICE, data).await)
        );
        let Apdu::Error(error) = response else {
            panic!("{response:?}")
        };
        assert_eq!((error.error_class, error.error_code), result);
        settle().await;
        assert_eq!(
            records(&fixture),
            vec![expected_query(target, 77, 0, Some(result))]
        );
        assert_eq!(reads.load(Ordering::Acquire), 0);
        assert_idle(&fixture);
        fixture.server.stop().await.unwrap();
        plain.server.stop().await.unwrap();
    }
}

#[tokio::test]
async fn audit_reporter_query_read_bit_levels_and_monitored_objects_gate_success_and_error() {
    for (level, bit, selection, selected) in [
        (AuditLevel::NONE, true, None, false),
        (AuditLevel::AUDIT_ALL, false, None, false),
        (AuditLevel::AUDIT_CONFIG, true, None, true),
        (AuditLevel::AUDIT_ALL, true, None, true),
        (AuditLevel::AUDIT_ALL, true, Some(vec![]), false),
        (
            AuditLevel::AUDIT_ALL,
            true,
            Some(vec![Selector::None]),
            false,
        ),
        (
            AuditLevel::AUDIT_ALL,
            true,
            Some(vec![Selector::Object(target()); 2]),
            true,
        ),
        (
            AuditLevel::AUDIT_ALL,
            true,
            Some(vec![Selector::ObjectType(ObjectType::AUDIT_LOG)]),
            true,
        ),
        (
            AuditLevel::AUDIT_ALL,
            true,
            Some(vec![Selector::Object(oid(ObjectType::AUDIT_LOG, 99))]),
            false,
        ),
        (
            AuditLevel::AUDIT_ALL,
            true,
            Some(vec![Selector::ObjectType(ObjectType::FILE)]),
            false,
        ),
    ] {
        for mode in ["real", "no capability"] {
            let mut reporter = read_reporter();
            reporter.set_audit_level(level).unwrap();
            if !bit {
                reporter
                    .set_auditable_operations(AuditOperationFlags::empty())
                    .unwrap();
            }
            reporter.set_monitored_objects(selection.clone()).unwrap();
            reporter
                .set_audit_priority_filter(BACnetPriorityFilter::empty())
                .unwrap();
            let mut fixture = server(reporter).await;
            add_log(&fixture, 0, mode).await;
            let response = dispatch(&fixture.server, SERVICE, encode(&query(None, 1))).await;
            let result = if mode == "real" {
                assert!(matches!(response, Apdu::ComplexAck(_)));
                None
            } else {
                assert!(matches!(response, Apdu::Error(_)));
                Some((
                    ErrorClass::SERVICES,
                    ErrorCode::OPTIONAL_FUNCTIONALITY_NOT_SUPPORTED,
                ))
            };
            settle().await;
            assert_eq!(
                records(&fixture),
                if selected {
                    vec![expected_query(target(), 77, 0, result)]
                } else {
                    vec![]
                }
            );
            assert_idle(&fixture);
            fixture.server.stop().await.unwrap();
        }
    }
}