bacnet-server 0.15.0

BACnet server: APDU dispatch and service handlers
Documentation
use super::*;
use bacnet_objects::notification_class::NotificationClass;
use bacnet_types::{
    bitstring::{DaysOfWeek, EventTransitionBits},
    constructed::BACnetDestination,
    primitives::Time,
};

#[tokio::test]
async fn audit_empty_values_wp_recipient_list_preserves_present_empty_and_null() {
    let mut fixture = server(reporter()).await;
    let target = oid(ObjectType::NOTIFICATION_CLASS, 1);
    let destination = BACnetDestination {
        valid_days: DaysOfWeek::all(),
        from_time: Time {
            hour: 0,
            minute: 0,
            second: 0,
            hundredths: 0,
        },
        to_time: Time {
            hour: 23,
            minute: 59,
            second: 0,
            hundredths: 0,
        },
        recipient: BACnetRecipient::Device(oid(ObjectType::DEVICE, 20)),
        process_identifier: 1,
        issue_confirmed_notifications: false,
        transitions: EventTransitionBits::all(),
    };
    let mut populated = BytesMut::new();
    bacnet_encoding::constructed::encode_destination_list(
        &mut populated,
        std::slice::from_ref(&destination),
    )
    .unwrap();
    assert!(populated.len() <= 32);
    let mut object = NotificationClass::new(1, "empty-audit-destinations").unwrap();
    object.add_destination(destination).unwrap();
    fixture
        .server
        .db
        .write()
        .await
        .add(Box::new(object))
        .unwrap();
    for (index, previous) in [populated.to_vec(), vec![]].into_iter().enumerate() {
        let response = dispatch(
            &fixture.server,
            ConfirmedServiceChoice::WRITE_PROPERTY,
            wp(target, PropertyIdentifier::RECIPIENT_LIST, vec![], None),
        )
        .await;
        assert!(matches!(response, Apdu::SimpleAck(_)), "{response:?}");
        assert_eq!(
            fixture
                .server
                .db
                .read()
                .await
                .get(&target)
                .unwrap()
                .read_property(PropertyIdentifier::RECIPIENT_LIST, None)
                .unwrap(),
            PropertyValue::ApplicationData(vec![])
        );
        settle().await;
        let records = notifications(&fixture.transport.sent);
        assert_eq!(records.len(), index + 1);
        assert_eq!(records[index].notifications.len(), 1);
        let record = &records[index].notifications[0];
        assert_eq!(record.operation, AuditOperation::WRITE);
        assert_eq!(record.invoke_id, Some(77 + index as u8));
        assert_eq!(record.target_object, Some(target));
        assert_eq!(
            record.target_property,
            Some(AuditPropertyReference {
                property_identifier: PropertyIdentifier::RECIPIENT_LIST,
                property_array_index: None
            })
        );
        assert_eq!(record.target_value, Some(vec![]));
        assert_eq!(record.current_value, Some(previous));
        assert_eq!(record.result, None);
    }
    // Encoded NULL is a present one-octet value, not an empty destination
    // list. Recipient_List isn't commandable and has no NULL in its
    // datatype, so the NULL succeeds unchanged (#1396), and the record is
    // the successful write the requester was answered with.
    let response = dispatch(
        &fixture.server,
        ConfirmedServiceChoice::WRITE_PROPERTY,
        wp(target, PropertyIdentifier::RECIPIENT_LIST, vec![0], None),
    )
    .await;
    assert!(
        matches!(response, Apdu::SimpleAck(_)),
        "expected the NULL to succeed: {response:?}"
    );
    settle().await;
    let records = notifications(&fixture.transport.sent);
    assert_eq!(records.len(), 3);
    let record = &records[2].notifications[0];
    assert_eq!(record.target_value, Some(vec![0]));
    assert_eq!(record.current_value, Some(vec![]));
    assert_eq!(record.result, None);
    assert_eq!(
        fixture
            .server
            .db
            .read()
            .await
            .get(&target)
            .unwrap()
            .read_property(PropertyIdentifier::RECIPIENT_LIST, None)
            .unwrap(),
        PropertyValue::ApplicationData(vec![])
    );
    fixture.server.stop().await.unwrap();
}

#[test]
fn audit_empty_values_small_known_values_preserve_zero_and_bound_32() {
    use super::audit_reporter::small_value;
    for empty in [
        PropertyValue::List(vec![]),
        PropertyValue::ApplicationData(vec![]),
    ] {
        assert_eq!(small_value(&empty), Some(vec![]));
    }
    for length in [32, 33] {
        for value in [
            PropertyValue::List(vec![PropertyValue::Null; length]),
            PropertyValue::ApplicationData(vec![0; length]),
        ] {
            assert_eq!(small_value(&value), (length <= 32).then(|| vec![0; length]));
        }
    }
    assert_eq!(small_value(&PropertyValue::Null), Some(vec![0]));
}