use super::{SegKey, SegmentedReceiveState};
use bacnet_transport::port::TransportPort;
use bacnet_transport::port::TransportProvenance;
use bacnet_transport::sc::{LoopbackWebSocket, ScTransport, WebSocketPort};
use bacnet_transport::sc_frame::{ScFunction, ScMessage, decode_sc_message, encode_sc_message};
use bacnet_types::MacAddr;
use bytes::{Bytes, BytesMut};
use std::collections::HashMap;
use tokio::time::{Duration, timeout};
async fn verified_relayed_provenance() -> TransportProvenance {
let (ws_client, ws_hub) = LoopbackWebSocket::pair();
let mut transport = ScTransport::new(ws_client, [0x01; 6]).with_device_uuid([1; 16]);
let hub_task = tokio::spawn(async move {
let data = ws_hub.recv().await.unwrap();
let req = decode_sc_message(&data).unwrap();
let mut payload = Vec::with_capacity(26);
payload.extend_from_slice(&[0x10; 6]);
payload.extend_from_slice(&[0x33; 16]);
payload.extend_from_slice(&1476u16.to_be_bytes());
payload.extend_from_slice(&1476u16.to_be_bytes());
let accept = ScMessage {
function: ScFunction::ConnectAccept,
message_id: req.message_id,
originating_vmac: None,
destination_vmac: None,
dest_options: Vec::new(),
data_options: Vec::new(),
payload: Bytes::from(payload),
};
let mut buf = BytesMut::new();
encode_sc_message(&mut buf, &accept);
ws_hub.send(&buf).await.unwrap();
ws_hub
});
let mut rx = transport.start().await.unwrap();
let ws_hub = hub_task.await.unwrap();
let msg = ScMessage {
function: ScFunction::EncapsulatedNpdu,
message_id: 0x2233,
originating_vmac: Some([0x22; 6]),
destination_vmac: None,
dest_options: Vec::new(),
data_options: Vec::new(),
payload: Bytes::from_static(&[0x01, 0x00, 0x30]),
};
let mut buf = BytesMut::new();
encode_sc_message(&mut buf, &msg);
ws_hub.send(&buf).await.unwrap();
let received = timeout(Duration::from_secs(2), rx.recv())
.await
.expect("hub relay timed out")
.expect("hub closed");
let provenance = received.provenance;
assert!(provenance.is_relayed_origin());
transport.stop().await.unwrap();
provenance
}
#[test]
fn seg_key_gains_provenance_comparison_for_isolation() {
let mac = MacAddr::from_slice(&[0x02]);
let unverified = TransportProvenance::unverified();
let a: SegKey = (mac.clone(), 7, unverified);
assert_eq!(a, (mac.clone(), 7, TransportProvenance::unverified()));
assert_ne!(a.0, MacAddr::from_slice(&[0x03]));
}
#[tokio::test]
async fn cross_segment_provenance_mismatch_is_detected_fail_closed() {
let verified = verified_relayed_provenance().await;
let unverified = TransportProvenance::unverified();
let tsm_mac = MacAddr::from_slice(&[0x02]);
let invoke_id = 7;
let mut keys: HashMap<SegKey, ()> = HashMap::new();
keys.insert((tsm_mac.clone(), invoke_id, unverified.clone()), ());
let conflict = keys.keys().find(|existing| {
existing.0 == tsm_mac && existing.1 == invoke_id && existing.2 != verified
});
assert!(conflict.is_some(), "mismatch must be detected");
let no_conflict = keys.keys().find(|existing| {
existing.0 == tsm_mac && existing.1 == invoke_id && existing.2 != unverified
});
assert!(no_conflict.is_none());
}
#[test]
fn routed_source_stays_a_claim_compat_mode() {
let mac = MacAddr::from_slice(&[0x09]);
let a: SegKey = (mac.clone(), 7, TransportProvenance::unverified());
let b: SegKey = (mac, 7, TransportProvenance::unverified());
assert_eq!(a, b);
}
#[test]
fn state_snapshot_type_threads_provenance() {
fn assert_clone<T: Clone>() {}
assert_clone::<TransportProvenance>();
let _ = std::mem::size_of::<SegmentedReceiveState>();
}