use super::durable_stop_tests::{send, stop_then_release};
use super::durable_write_wire_tests::{
ERROR_PDU, HeldStorage, SIMPLE_ACK_WPM, SIMPLE_ACK_WRITE, WAIT, while_saving,
};
use super::mutation_list_wire_tests::wire;
use super::mutation_tests::{Fixture, value, wpm};
use super::*;
use crate::server::test_transport::TestTransport;
use PropertyIdentifier as P;
use bacnet_encoding::constructed::encode_access_rule;
use bacnet_objects::access_control::{
AccessRightsObject, AccessRightsPersistence, AccessRightsSnapshot,
};
use bacnet_objects::device::{DeviceConfig, DeviceObject};
use bacnet_objects::traits::BACnetObject;
use bacnet_services::common::BACnetPropertyValue;
use bacnet_services::wpm::WriteAccessSpecification;
use bacnet_services::write_property::WritePropertyRequest;
use bacnet_types::constructed::{BACnetAccessRule, BACnetDeviceObjectPropertyReference};
use bacnet_types::enums::{AccessRuleLocationSpecifier, AccessRuleTimeRangeSpecifier};
use std::sync::atomic::Ordering;
use std::time::Duration;
type RightsStorage = HeldStorage<AccessRightsSnapshot>;
impl AccessRightsPersistence for RightsStorage {
fn load(&self, _rights: ObjectIdentifier) -> Result<Option<AccessRightsSnapshot>, Error> {
Ok(self.load_saved())
}
fn save(
&self,
_rights: ObjectIdentifier,
snapshot: &AccessRightsSnapshot,
) -> Result<(), Error> {
self.store(snapshot)
}
}
const WRITE_REFUSED: [u8; 7] = [0x50, 5, 15, 0x91, 0, 0x91, 25];
fn zone_rule(instance: u32) -> BACnetAccessRule {
let zone = ObjectIdentifier::new(ObjectType::ACCESS_ZONE, instance).unwrap();
BACnetAccessRule::new(None, Some(zone.into()), true)
}
fn grown_rule() -> BACnetAccessRule {
let unspecified =
|object_type| ObjectIdentifier::new(object_type, ObjectIdentifier::MAX_INSTANCE).unwrap();
BACnetAccessRule {
time_range_specifier: AccessRuleTimeRangeSpecifier::SPECIFIED,
time_range: Some(BACnetDeviceObjectPropertyReference::new_local(
unspecified(ObjectType::SCHEDULE),
P::PRESENT_VALUE.to_raw(),
)),
location_specifier: AccessRuleLocationSpecifier::SPECIFIED,
location: Some(unspecified(ObjectType::ACCESS_POINT).into()),
enable: false,
}
}
fn encoded(rules: &[BACnetAccessRule]) -> Vec<u8> {
let mut buf = BytesMut::new();
for rule in rules {
encode_access_rule(&mut buf, rule);
}
buf.to_vec()
}
fn served_rules(rules: &[BACnetAccessRule]) -> PropertyValue {
PropertyValue::List(
rules
.iter()
.map(|rule| PropertyValue::ApplicationData(encoded(std::slice::from_ref(rule))))
.collect(),
)
}
fn holding(snapshot: AccessRightsSnapshot) -> Arc<RightsStorage> {
let storage = Arc::new(RightsStorage::default());
*storage.saved.lock().unwrap() = Some(snapshot);
storage
}
fn positive_only(rules: &[BACnetAccessRule]) -> AccessRightsSnapshot {
AccessRightsSnapshot {
positive_access_rules: Some(rules.to_vec()),
..AccessRightsSnapshot::default()
}
}
fn rights_object(storage: &Arc<RightsStorage>) -> AccessRightsObject {
AccessRightsObject::with_persistence(
1,
"AR",
Arc::clone(storage) as Arc<dyn AccessRightsPersistence>,
)
.unwrap()
}
async fn served_by(storage: &Arc<RightsStorage>) -> (Arc<Fixture>, ObjectIdentifier) {
let fixture = Fixture::new(None);
let rights = rights_object(storage);
let oid = rights.object_identifier();
fixture.db.write().await.add(Box::new(rights)).unwrap();
(Arc::new(fixture), oid)
}
async fn reads(fixture: &Fixture, rights: ObjectIdentifier) -> Vec<PropertyValue> {
let mut values = Vec::new();
for property in [
P::POSITIVE_ACCESS_RULES,
P::NEGATIVE_ACCESS_RULES,
P::LOG_ENABLE,
] {
values.push(fixture.read(rights, property).await);
}
values
}
fn expected_reads(snapshot: &AccessRightsSnapshot) -> Vec<PropertyValue> {
let rules =
|rules: &Option<Vec<BACnetAccessRule>>| served_rules(rules.as_deref().unwrap_or(&[]));
vec![
rules(&snapshot.positive_access_rules),
rules(&snapshot.negative_access_rules),
PropertyValue::Boolean(snapshot.enable.unwrap_or(true)),
]
}
fn write_property(
rights: ObjectIdentifier,
property: P,
index: Option<u32>,
property_value: Vec<u8>,
) -> Bytes {
let mut request = BytesMut::new();
WritePropertyRequest {
object_identifier: rights,
property_identifier: property,
property_array_index: index,
property_value,
priority: None,
}
.encode(&mut request)
.unwrap();
request.freeze()
}
fn attempt(property: P, index: Option<u32>, value: Vec<u8>) -> BACnetPropertyValue {
BACnetPropertyValue {
property_identifier: property,
property_array_index: index,
value,
priority: None,
}
}
fn write_property_multiple(rights: ObjectIdentifier, attempts: Vec<BACnetPropertyValue>) -> Bytes {
wpm(vec![WriteAccessSpecification {
object_identifier: rights,
list_of_properties: attempts,
}])
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn each_saved_write_property_saves_off_the_lock_and_survives_a_rebuild() {
let earlier = positive_only(&[zone_rule(1), zone_rule(2)]);
for (property, index, octets, expected) in [
(
P::POSITIVE_ACCESS_RULES,
None,
encoded(&[zone_rule(3), zone_rule(4)]),
positive_only(&[zone_rule(3), zone_rule(4)]),
),
(
P::POSITIVE_ACCESS_RULES,
Some(2),
encoded(&[zone_rule(5)]),
positive_only(&[zone_rule(1), zone_rule(5)]),
),
(
P::NEGATIVE_ACCESS_RULES,
Some(0),
value(PropertyValue::Unsigned(2)),
AccessRightsSnapshot {
negative_access_rules: Some(vec![grown_rule(), grown_rule()]),
..earlier.clone()
},
),
(
P::LOG_ENABLE,
None,
value(PropertyValue::Boolean(false)),
AccessRightsSnapshot {
enable: Some(false),
..earlier.clone()
},
),
] {
let storage = holding(earlier.clone());
let (fixture, rights) = served_by(&storage).await;
let request = write_property(rights, property, index, octets);
let service = ConfirmedServiceChoice::WRITE_PROPERTY;
let (response, available) = while_saving(&fixture, &storage, service, request, WAIT).await;
assert_eq!(response, SIMPLE_ACK_WRITE, "{property:?} {index:?}");
assert!(available, "the database was held while {property:?} saved");
assert_eq!(storage.saves.load(Ordering::SeqCst), 1);
assert_eq!(storage.load_saved(), Some(expected.clone()));
assert_eq!(reads(&fixture, rights).await, expected_reads(&expected));
drop(fixture);
let (rebuilt, rights) = served_by(&storage).await;
assert_eq!(reads(&rebuilt, rights).await, expected_reads(&expected));
}
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn a_write_property_multiple_with_several_rights_writes_saves_once_and_survives_a_rebuild() {
let storage = Arc::new(RightsStorage::default());
let (fixture, rights) = served_by(&storage).await;
let positive = [zone_rule(1), zone_rule(2)];
let request = write_property_multiple(
rights,
vec![
attempt(P::POSITIVE_ACCESS_RULES, None, encoded(&positive)),
attempt(
P::NEGATIVE_ACCESS_RULES,
Some(0),
value(PropertyValue::Unsigned(1)),
),
attempt(P::LOG_ENABLE, None, value(PropertyValue::Boolean(false))),
],
);
let (started, go) = storage.hold();
let sending = tokio::spawn({
let fixture = Arc::clone(&fixture);
async move {
wire(
&fixture,
ConfirmedServiceChoice::WRITE_PROPERTY_MULTIPLE,
request,
)
.await
}
});
tokio::task::spawn_blocking(move || started.recv_timeout(WAIT))
.await
.unwrap()
.expect("the save started");
let readable = tokio::time::timeout(WAIT, fixture.db.read()).await.is_ok();
let writable = tokio::time::timeout(WAIT, fixture.db.write()).await.is_ok();
drop(go);
assert_eq!(sending.await.unwrap(), SIMPLE_ACK_WPM);
assert!(readable && writable, "the database was held while it saved");
assert_eq!(storage.saves.load(Ordering::SeqCst), 1);
let expected = AccessRightsSnapshot {
positive_access_rules: Some(positive.to_vec()),
negative_access_rules: Some(vec![grown_rule()]),
enable: Some(false),
accompaniment: None,
};
assert_eq!(storage.load_saved(), Some(expected.clone()));
drop(fixture);
let (rebuilt, rights) = served_by(&storage).await;
assert_eq!(reads(&rebuilt, rights).await, expected_reads(&expected));
}
async fn server(
storage: &Arc<RightsStorage>,
) -> (
BACnetServer<TestTransport>,
tokio::sync::mpsc::Sender<bacnet_transport::port::ReceivedNpdu>,
) {
let (transport, inbound) = TestTransport::inbound(4);
let mut db = ObjectDatabase::new();
db.add(Box::new(
DeviceObject::new(DeviceConfig {
identity: bacnet_objects::DeviceIdentity::new(100, 0)
.expect("valid Device identity")
.with_name("Access Rights device"),
..DeviceConfig::default()
})
.unwrap(),
))
.unwrap();
db.add(Box::new(rights_object(storage))).unwrap();
let server = BACnetServer::generic_builder()
.device(bacnet_objects::DeviceIdentity::new(100, 0).unwrap())
.transport(transport)
.database(db)
.enable_event_enrollment(false)
.build()
.await
.unwrap();
(server, inbound)
}
fn rights_oid() -> ObjectIdentifier {
ObjectIdentifier::new(ObjectType::ACCESS_RIGHTS, 1).unwrap()
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn a_write_local_saves_off_the_lock_and_survives_a_rebuild() {
let storage = Arc::new(RightsStorage::default());
let (server, _inbound) = server(&storage).await;
let server = Arc::new(server);
let negative = [zone_rule(7)];
let (started, go) = storage.hold();
let writing = tokio::spawn({
let server = Arc::clone(&server);
let value = PropertyValue::ApplicationData(encoded(&negative));
async move {
server
.write_local(
&rights_oid(),
P::NEGATIVE_ACCESS_RULES,
None,
value,
None,
crate::LocalCommandSource::ServerDevice,
)
.await
}
});
tokio::task::spawn_blocking(move || started.recv_timeout(WAIT))
.await
.unwrap()
.expect("the save started");
let db = Arc::clone(server.database());
let readable = tokio::time::timeout(WAIT, db.read()).await.is_ok();
let writable = tokio::time::timeout(WAIT, db.write()).await.is_ok();
go.send(()).unwrap();
writing.await.unwrap().unwrap();
assert!(readable && writable, "the database was held while it saved");
assert_eq!(storage.saves.load(Ordering::SeqCst), 1);
drop(db);
let mut server = Arc::into_inner(server).expect("no other handle on the server");
server.stop().await.unwrap();
drop(server);
let (rebuilt, rights) = served_by(&storage).await;
assert_eq!(
rebuilt.read(rights, P::NEGATIVE_ACCESS_RULES).await,
served_rules(&negative)
);
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn a_state_that_cannot_be_saved_is_refused_on_every_path_and_nothing_changes() {
let kept = positive_only(&[zone_rule(1), zone_rule(2)]);
let storage = holding(kept.clone());
let (fixture, rights) = served_by(&storage).await;
storage.fail.store(true, Ordering::SeqCst);
for (property, index, octets) in [
(P::POSITIVE_ACCESS_RULES, None, encoded(&[zone_rule(3)])),
(P::POSITIVE_ACCESS_RULES, Some(1), encoded(&[zone_rule(3)])),
(
P::NEGATIVE_ACCESS_RULES,
Some(0),
value(PropertyValue::Unsigned(2)),
),
(P::LOG_ENABLE, None, value(PropertyValue::Boolean(false))),
] {
let request = write_property(rights, property, index, octets);
assert_eq!(
wire(&fixture, ConfirmedServiceChoice::WRITE_PROPERTY, request).await,
WRITE_REFUSED,
"{property:?} {index:?}"
);
}
let request = write_property_multiple(
rights,
vec![attempt(
P::LOG_ENABLE,
None,
value(PropertyValue::Boolean(false)),
)],
);
let response = wire(
&fixture,
ConfirmedServiceChoice::WRITE_PROPERTY_MULTIPLE,
request,
)
.await;
assert_eq!(response[..8], [0x50, 5, 16, 0x0E, 0x91, 0, 0x91, 25]);
assert_eq!(reads(&fixture, rights).await, expected_reads(&kept));
assert_eq!(storage.load_saved(), Some(kept.clone()));
drop(fixture);
storage.fail.store(false, Ordering::SeqCst);
let (rebuilt, rights) = served_by(&storage).await;
assert_eq!(reads(&rebuilt, rights).await, expected_reads(&kept));
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn a_write_property_multiple_refused_at_its_first_rights_write_stages_nothing() {
let storage = Arc::new(RightsStorage::default());
let (fixture, rights) = served_by(&storage).await;
let request = write_property_multiple(
rights,
vec![
attempt(P::LOG_ENABLE, None, value(PropertyValue::Unsigned(1))),
attempt(P::POSITIVE_ACCESS_RULES, None, encoded(&[zone_rule(9)])),
],
);
let response = wire(
&fixture,
ConfirmedServiceChoice::WRITE_PROPERTY_MULTIPLE,
request,
)
.await;
assert_eq!(response[0], ERROR_PDU, "the first attempt was refused");
saves_done(&fixture, rights).await;
assert_eq!(storage.saves.load(Ordering::SeqCst), 0);
assert_eq!(storage.load_saved(), None);
assert_eq!(
reads(&fixture, rights).await,
expected_reads(&AccessRightsSnapshot::default())
);
}
async fn saves_done(fixture: &Fixture, rights: ObjectIdentifier) {
let mut db = fixture.db.write().await;
let writes = db
.get_mut(&rights)
.and_then(|object| object.durable_writes_internal())
.expect("the object saves");
assert!(!writes.has_staged_write(), "a write is still staged");
let wait = writes.settle_forgotten_writes().expect("the object saves");
drop(db);
tokio::time::timeout(WAIT, wait)
.await
.expect("the saves ran");
}
#[tokio::test(start_paused = true)]
async fn a_paused_clock_stands_still_while_a_rights_save_runs() {
let storage = Arc::new(RightsStorage::default());
let (fixture, rights) = served_by(&storage).await;
let (started, go) = storage.hold();
let releasing = std::thread::spawn(move || {
started.recv_timeout(WAIT).expect("the save started");
std::thread::sleep(Duration::from_millis(50));
go.send(()).unwrap();
});
let timer = tokio::spawn(tokio::time::sleep(Duration::from_secs(3)));
let start = tokio::time::Instant::now();
let request = write_property(
rights,
P::LOG_ENABLE,
None,
value(PropertyValue::Boolean(false)),
);
assert_eq!(
wire(&fixture, ConfirmedServiceChoice::WRITE_PROPERTY, request).await,
SIMPLE_ACK_WRITE
);
releasing.join().unwrap();
assert!(
start.elapsed() < Duration::from_secs(3),
"virtual time jumped {:?} while the save ran",
start.elapsed()
);
assert!(!timer.is_finished());
timer.abort();
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn a_server_stopped_mid_save_leaves_storage_with_the_served_rules() {
let served = positive_only(&[zone_rule(1)]);
let storage = holding(served.clone());
let (server, inbound) = server(&storage).await;
let (started, go) = storage.hold();
let request = write_property(
rights_oid(),
P::POSITIVE_ACCESS_RULES,
Some(1),
encoded(&[zone_rule(9)]),
);
send(&inbound, ConfirmedServiceChoice::WRITE_PROPERTY, request).await;
tokio::task::spawn_blocking(move || started.recv_timeout(WAIT))
.await
.unwrap()
.expect("the save started");
let server = stop_then_release(server, go).await;
assert_eq!(storage.load_saved(), Some(served.clone()));
let db = Arc::clone(server.database());
assert_eq!(
db.read()
.await
.get(&rights_oid())
.unwrap()
.read_property(P::POSITIVE_ACCESS_RULES, None)
.unwrap(),
served_rules(&[zone_rule(1)])
);
drop(db);
drop(server);
assert_eq!(
rights_object(&storage).positive_access_rules(),
[zone_rule(1)]
);
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn a_null_succeeds_unchanged_and_saves_nothing() {
let kept = AccessRightsSnapshot {
positive_access_rules: Some(vec![zone_rule(1)]),
enable: Some(false),
..AccessRightsSnapshot::default()
};
let storage = holding(kept.clone());
let (fixture, rights) = served_by(&storage).await;
let null = || value(PropertyValue::Null);
for (property, index) in [
(P::LOG_ENABLE, None),
(P::POSITIVE_ACCESS_RULES, None),
(P::POSITIVE_ACCESS_RULES, Some(1)),
(P::NEGATIVE_ACCESS_RULES, Some(0)),
] {
let request = write_property(rights, property, index, null());
assert_eq!(
wire(&fixture, ConfirmedServiceChoice::WRITE_PROPERTY, request).await,
SIMPLE_ACK_WRITE,
"{property:?} {index:?}"
);
}
let request = write_property_multiple(rights, vec![attempt(P::LOG_ENABLE, None, null())]);
assert_eq!(
wire(
&fixture,
ConfirmedServiceChoice::WRITE_PROPERTY_MULTIPLE,
request
)
.await,
SIMPLE_ACK_WPM
);
assert_eq!(reads(&fixture, rights).await, expected_reads(&kept));
assert_eq!(storage.saves.load(Ordering::SeqCst), 0);
assert_eq!(storage.load_saved(), Some(kept));
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn an_accompaniment_write_saves_off_the_lock_and_survives_a_rebuild() {
const REMOTE_CREDENTIAL: [u8; 10] =
[0x0C, 0x02, 0x00, 0x00, 0x63, 0x1C, 0x08, 0x00, 0x00, 0x05];
let oid = |object_type, instance| ObjectIdentifier::new(object_type, instance).unwrap();
let storage = holding(AccessRightsSnapshot {
accompaniment: Some(oid(ObjectType::ACCESS_USER, 3).into()),
..AccessRightsSnapshot::default()
});
let (fixture, rights) = served_by(&storage).await;
let request = write_property(rights, P::ACCOMPANIMENT, None, REMOTE_CREDENTIAL.to_vec());
let service = ConfirmedServiceChoice::WRITE_PROPERTY;
let (response, available) = while_saving(&fixture, &storage, service, request, WAIT).await;
assert_eq!(response, SIMPLE_ACK_WRITE);
assert!(available, "the database was held while Accompaniment saved");
assert_eq!(storage.saves.load(Ordering::SeqCst), 1);
let credential = bacnet_types::constructed::BACnetDeviceObjectReference {
device_identifier: Some(oid(ObjectType::DEVICE, 99)),
object_identifier: oid(ObjectType::ACCESS_CREDENTIAL, 5),
};
assert_eq!(
storage.load_saved().and_then(|saved| saved.accompaniment),
Some(credential)
);
let served = PropertyValue::ApplicationData(REMOTE_CREDENTIAL.to_vec());
assert_eq!(fixture.read(rights, P::ACCOMPANIMENT).await, served);
let null = || value(PropertyValue::Null);
let request = write_property(rights, P::ACCOMPANIMENT, None, null());
assert_eq!(wire(&fixture, service, request).await, SIMPLE_ACK_WRITE);
let request = write_property_multiple(rights, vec![attempt(P::ACCOMPANIMENT, None, null())]);
assert_eq!(
wire(
&fixture,
ConfirmedServiceChoice::WRITE_PROPERTY_MULTIPLE,
request
)
.await,
SIMPLE_ACK_WPM
);
assert_eq!(storage.saves.load(Ordering::SeqCst), 1);
assert_eq!(fixture.read(rights, P::ACCOMPANIMENT).await, served);
drop(fixture);
let (rebuilt, rights) = served_by(&storage).await;
assert_eq!(rebuilt.read(rights, P::ACCOMPANIMENT).await, served);
}
#[path = "access_rights_fold_wire_tests.rs"]
mod fold_wire_tests;