use super::control_messages::handle_network_message;
use super::envelope_harness::*;
use super::*;
use crate::router_table::ReachabilityStatus;
use bacnet_encoding::npdu::{decode_npdu, NpduAddress};
use bacnet_types::enums::RejectMessageReason;
#[tokio::test]
async fn who_is_local_origin_adds_ingress_source_and_attributes_on_forward() {
let mut h = Harness::two_port();
let mut npdu = control_npdu(NetworkMessageType::WHO_IS_ROUTER_TO_NETWORK, &[0x0b, 0xb8]);
let mut ctx = h.ctx(0, &[7], npdu.clone());
ctx.data_attributes = attributes();
npdu = ctx.npdu.clone();
h.handle(ctx).await;
assert!(h.drain(0).is_empty());
let forwarded = broadcast_data(h.drain(1));
assert_eq!(forwarded.len(), 1);
let relayed = decode_npdu(forwarded[0].clone()).unwrap();
assert_eq!(
relayed.source,
Some(NpduAddress {
network: 1000,
mac_address: MacAddr::from_slice(&[7]),
})
);
assert!(relayed.destination.is_none());
assert_eq!(relayed.payload, npdu.payload);
assert_eq!(
relayed.message_type,
Some(NetworkMessageType::WHO_IS_ROUTER_TO_NETWORK.to_raw())
);
assert_eq!(h.table.lock().await.len(), 2);
}
#[tokio::test]
async fn who_is_routed_origin_retains_source() {
let mut h = Harness::two_port();
let mut npdu = control_npdu(NetworkMessageType::WHO_IS_ROUTER_TO_NETWORK, &[0x0b, 0xb8]);
npdu.source = Some(NpduAddress {
network: 900,
mac_address: MacAddr::from_slice(&[9]),
});
let mut ctx = h.ctx(0, &[7], npdu);
ctx.data_attributes = attributes();
h.handle(ctx).await;
let forwarded = broadcast_data(h.drain(1));
assert_eq!(forwarded.len(), 1);
let relayed = decode_npdu(forwarded[0].clone()).unwrap();
assert_eq!(
relayed.source,
Some(NpduAddress {
network: 900,
mac_address: MacAddr::from_slice(&[9]),
})
);
}
#[tokio::test]
async fn who_is_scoped_known_network_answered_locally_not_forwarded() {
let mut h = Harness::two_port();
let mut npdu = control_npdu(NetworkMessageType::WHO_IS_ROUTER_TO_NETWORK, &[0x07, 0xd0]);
let mut ctx = h.ctx(0, &[7], npdu.clone());
ctx.data_attributes = attributes();
npdu = ctx.npdu.clone();
h.handle(ctx).await;
assert!(h.drain(1).is_empty());
let mut replies = h.drain(0);
assert_eq!(replies.len(), 1);
let reply = replies.pop().unwrap();
let (data, attrs) = match reply {
SendRequest::Broadcast {
npdu,
data_attributes,
} => (npdu, data_attributes),
SendRequest::Unicast { .. } => panic!("expected broadcast I-Am reply"),
};
let decoded = decode_npdu(data).unwrap();
assert_eq!(
decoded.message_type,
Some(NetworkMessageType::I_AM_ROUTER_TO_NETWORK.to_raw())
);
assert_eq!(decoded.payload.as_ref(), &[0x07, 0xd0]);
assert_eq!(attrs, attributes());
assert_eq!(npdu.payload.as_ref(), &[0x07, 0xd0]);
}
#[tokio::test]
async fn who_is_unscoped_lists_reachable_not_ingress() {
let mut h = Harness::two_port();
h.handle(h.ctx(
0,
&[7],
control_npdu(NetworkMessageType::WHO_IS_ROUTER_TO_NETWORK, &[]),
))
.await;
assert!(h.drain(1).is_empty());
let replies = broadcast_data(h.drain(0));
assert_eq!(replies.len(), 1);
let decoded = decode_npdu(replies[0].clone()).unwrap();
assert_eq!(decoded.payload.as_ref(), &[0x07, 0xd0]);
}
#[tokio::test]
async fn who_is_truncated_tail_never_promotes_to_global_scan() {
let mut h = Harness::two_port();
for payload in [&[0x0b][..], &[0x0b, 0xb8, 0x00][..], &[0, 0][..]] {
h.handle(h.ctx(
0,
&[7],
control_npdu(NetworkMessageType::WHO_IS_ROUTER_TO_NETWORK, payload),
))
.await;
h.assert_quiet();
}
assert_eq!(h.table.lock().await.len(), 2);
}
#[tokio::test]
async fn init_update_query_and_garbage_envelopes() {
let mut h = Harness::two_port();
h.handle(h.ctx(
1,
&[2],
control_npdu(
NetworkMessageType::INITIALIZE_ROUTING_TABLE,
&[1, 0x0b, 0xb8],
),
))
.await;
h.assert_quiet();
assert_eq!(h.table.lock().await.len(), 2);
h.handle(h.ctx(
1,
&[2],
control_npdu(NetworkMessageType::INITIALIZE_ROUTING_TABLE, &[0, 0xff]),
))
.await;
h.assert_quiet();
h.handle(h.ctx(
1,
&[2],
control_npdu(
NetworkMessageType::INITIALIZE_ROUTING_TABLE,
&[1, 0x0b, 0xb8, 1, 0],
),
))
.await;
assert!(h.drain(0).is_empty());
let mut acks = h.drain(1);
assert_eq!(acks.len(), 1);
match acks.pop().unwrap() {
SendRequest::Unicast { npdu, mac, .. } => {
assert_eq!(mac.as_slice(), &[2]);
let decoded = decode_npdu(npdu).unwrap();
assert_eq!(
decoded.message_type,
Some(NetworkMessageType::INITIALIZE_ROUTING_TABLE_ACK.to_raw())
);
assert!(decoded.payload.is_empty());
}
SendRequest::Broadcast { .. } => panic!("expected unicast ACK"),
}
let route = h.table.lock().await.lookup(3000).unwrap().clone();
assert_eq!(route.port_index, 0);
assert!(!route.directly_connected);
assert_eq!(route.next_hop_mac.as_slice(), &[2]);
h.handle(h.ctx(
1,
&[2],
control_npdu(NetworkMessageType::INITIALIZE_ROUTING_TABLE, &[0]),
))
.await;
let mut acks = h.drain(1);
assert_eq!(acks.len(), 1);
match acks.pop().unwrap() {
SendRequest::Unicast { npdu, .. } => {
let decoded = decode_npdu(npdu).unwrap();
assert_eq!(
decoded.payload.as_ref(),
&[3, 0x03, 0xe8, 1, 0, 0x07, 0xd0, 2, 0, 0x0b, 0xb8, 1, 0]
);
}
SendRequest::Broadcast { .. } => panic!("expected unicast ACK"),
}
}
#[tokio::test]
async fn init_ack_truncated_entry_learns_nothing() {
let mut h = Harness::two_port();
h.handle(h.ctx(
1,
&[2],
control_npdu(
NetworkMessageType::INITIALIZE_ROUTING_TABLE_ACK,
&[2, 0x0b, 0xb8, 0, 0, 0x0f],
),
))
.await;
h.assert_quiet();
let table = h.table.lock().await;
assert_eq!(table.len(), 2);
assert!(table.lookup(3000).is_none());
}
#[tokio::test]
async fn initialize_routing_table_ack() {
let mut table = RouterTable::new();
table.add_direct(1000, 0);
table.add_direct(2000, 1);
let table = Arc::new(Mutex::new(table));
let (tx, mut rx) = mpsc::channel::<SendRequest>(256);
let send_txs = vec![tx];
let npdu = Npdu {
is_network_message: true,
message_type: Some(NetworkMessageType::INITIALIZE_ROUTING_TABLE.to_raw()),
payload: Bytes::from_static(&[0]),
..Npdu::default()
};
handle_network_message(
&table,
&send_txs,
&IngressContext::test_local(0, 1000, &[0x0A], npdu),
&control_policy::ControlGate::permissive(),
&LocalControl::default(),
)
.await;
let sent = rx.try_recv().unwrap();
match sent {
SendRequest::Unicast {
npdu: data, mac, ..
} => {
assert_eq!(mac.as_slice(), &[0x0A]);
let decoded = decode_npdu(data.clone()).unwrap();
assert!(decoded.is_network_message);
assert_eq!(
decoded.message_type,
Some(NetworkMessageType::INITIALIZE_ROUTING_TABLE_ACK.to_raw())
);
assert_eq!(decoded.payload.len(), 9);
assert_eq!(decoded.payload[0], 2);
assert_eq!(
decoded.payload.as_ref(),
&[2, 0x03, 0xe8, 1, 0, 0x07, 0xd0, 2, 0]
);
}
_ => panic!("Expected Unicast response for Init-Routing-Table"),
}
}
#[tokio::test]
async fn initialize_routing_table_empty_payload_sends_no_ack() {
let mut table = RouterTable::new();
table.add_direct(1000, 0);
let table = Arc::new(Mutex::new(table));
let (tx, mut rx) = mpsc::channel::<SendRequest>(256);
let send_txs = vec![tx];
let npdu = Npdu {
is_network_message: true,
message_type: Some(NetworkMessageType::INITIALIZE_ROUTING_TABLE.to_raw()),
payload: Bytes::new(),
..Npdu::default()
};
handle_network_message(
&table,
&send_txs,
&IngressContext::test_local(0, 1000, &[0x0A], npdu),
&control_policy::ControlGate::permissive(),
&LocalControl::default(),
)
.await;
assert!(rx.try_recv().is_err());
assert_eq!(table.lock().await.len(), 1);
}
#[tokio::test]
async fn directed_control_routed_not_consumed() {
let mut table = RouterTable::new();
table.add_direct(1000, 0);
table.add_direct(2000, 1);
table.add_learned(3000, 1, MacAddr::from_slice(&[5]));
let mut h = Harness::with_table(table);
let mut npdu = control_npdu(NetworkMessageType::I_AM_ROUTER_TO_NETWORK, &[0x0f, 0xa0]);
npdu.destination = Some(NpduAddress {
network: 3000,
mac_address: MacAddr::from_slice(&[9]),
});
npdu.hop_count = 255;
let mut ctx = h.ctx(0, &[7], npdu);
ctx.data_attributes = attributes();
h.dispatch(ctx).await;
assert!(h.drain(0).is_empty());
let mut forwarded = h.drain(1);
assert_eq!(forwarded.len(), 1);
match forwarded.pop().unwrap() {
SendRequest::Unicast {
npdu,
mac,
data_attributes,
} => {
assert_eq!(mac.as_slice(), &[5]);
let decoded = decode_npdu(npdu).unwrap();
assert_eq!(
decoded.message_type,
Some(NetworkMessageType::I_AM_ROUTER_TO_NETWORK.to_raw())
);
assert_eq!(
decoded.destination,
Some(NpduAddress {
network: 3000,
mac_address: MacAddr::from_slice(&[9]),
})
);
assert_eq!(decoded.hop_count, 254);
assert_eq!(decoded.payload.as_ref(), &[0x0f, 0xa0]);
assert_eq!(data_attributes, attributes());
}
SendRequest::Broadcast { .. } => panic!("expected unicast route"),
}
let table = h.table.lock().await;
assert!(table.lookup(4000).is_none());
assert_eq!(table.lookup(3000).unwrap().port_index, 1);
}
#[tokio::test]
async fn directed_control_to_own_network_handled_locally() {
let mut h = Harness::two_port();
let mut npdu = control_npdu(NetworkMessageType::WHO_IS_ROUTER_TO_NETWORK, &[0x07, 0xd0]);
npdu.destination = Some(NpduAddress {
network: 1000,
mac_address: MacAddr::from_slice(&[1]),
});
npdu.hop_count = 255;
h.dispatch(h.ctx(0, &[7], npdu)).await;
assert!(h.drain(1).is_empty());
let replies = broadcast_data(h.drain(0));
assert_eq!(replies.len(), 1);
assert_eq!(
decode_npdu(replies[0].clone()).unwrap().payload.as_ref(),
&[0x07, 0xd0]
);
}
#[tokio::test]
async fn directed_control_to_unknown_network_rejected() {
let mut h = Harness::two_port();
let mut npdu = control_npdu(NetworkMessageType::WHO_IS_ROUTER_TO_NETWORK, &[0x27, 0x0f]);
npdu.destination = Some(NpduAddress {
network: 9999,
mac_address: MacAddr::from_slice(&[9]),
});
npdu.hop_count = 255;
h.dispatch(h.ctx(0, &[7], npdu)).await;
let solicited = broadcast_data(h.drain(1));
assert_eq!(solicited.len(), 1);
let who_is = decode_npdu(solicited[0].clone()).unwrap();
assert_eq!(
who_is.message_type,
Some(NetworkMessageType::WHO_IS_ROUTER_TO_NETWORK.to_raw())
);
assert_eq!(who_is.payload.as_ref(), &[0x27, 0x0f]);
let mut rejects = h.drain(0);
assert_eq!(rejects.len(), 1);
match rejects.pop().unwrap() {
SendRequest::Unicast { npdu, mac, .. } => {
assert_eq!(mac.as_slice(), &[7]);
let decoded = decode_npdu(npdu).unwrap();
assert_eq!(
decoded.message_type,
Some(NetworkMessageType::REJECT_MESSAGE_TO_NETWORK.to_raw())
);
assert_eq!(
decoded.payload.as_ref(),
&[
RejectMessageReason::NOT_DIRECTLY_CONNECTED.to_raw(),
0x27,
0x0f
]
);
}
SendRequest::Broadcast { .. } => panic!("expected unicast reject"),
}
assert_eq!(h.table.lock().await.len(), 2);
}
#[tokio::test]
async fn directed_who_is_remote_broadcast_forwarded_with_origin() {
let mut h = Harness::two_port();
let mut npdu = control_npdu(NetworkMessageType::WHO_IS_ROUTER_TO_NETWORK, &[0x0b, 0xb8]);
npdu.destination = Some(NpduAddress {
network: 2000,
mac_address: MacAddr::new(),
});
npdu.hop_count = 255;
h.dispatch(h.ctx(0, &[7], npdu)).await;
assert!(h.drain(0).is_empty());
let forwarded = broadcast_data(h.drain(1));
assert_eq!(forwarded.len(), 1);
let decoded = decode_npdu(forwarded[0].clone()).unwrap();
assert!(decoded.destination.is_none());
assert_eq!(
decoded.source,
Some(NpduAddress {
network: 1000,
mac_address: MacAddr::from_slice(&[7]),
})
);
let mut npdu = control_npdu(NetworkMessageType::WHO_IS_ROUTER_TO_NETWORK, &[0x0b, 0xb8]);
npdu.destination = Some(NpduAddress {
network: 2000,
mac_address: MacAddr::new(),
});
npdu.source = Some(NpduAddress {
network: 900,
mac_address: MacAddr::from_slice(&[9]),
});
npdu.hop_count = 254;
h.dispatch(h.ctx(0, &[7], npdu)).await;
let forwarded = broadcast_data(h.drain(1));
assert_eq!(forwarded.len(), 1);
assert_eq!(
decode_npdu(forwarded[0].clone()).unwrap().source,
Some(NpduAddress {
network: 900,
mac_address: MacAddr::from_slice(&[9]),
})
);
}
#[tokio::test]
async fn link_broadcast_routed_unicast_control_stays_unicast() {
let mut table = RouterTable::new();
table.add_direct(1000, 0);
table.add_direct(2000, 1);
table.add_learned(3000, 1, MacAddr::from_slice(&[5]));
let mut h = Harness::with_table(table);
let mut npdu = control_npdu(NetworkMessageType::ROUTER_BUSY_TO_NETWORK, &[0x0b, 0xb8]);
npdu.destination = Some(NpduAddress {
network: 3000,
mac_address: MacAddr::from_slice(&[9]),
});
npdu.hop_count = 255;
let mut ctx = h.ctx(0, &[7], npdu);
ctx.link_layer_group = true;
h.dispatch(ctx).await;
assert!(h.drain(0).is_empty());
match h.drain(1).pop() {
Some(SendRequest::Unicast { .. }) => {}
other => panic!("expected unicast forward, got {other:?}"),
}
assert_eq!(
h.table.lock().await.effective_reachability(3000),
Some(ReachabilityStatus::Reachable)
);
}