use std::net::{IpAddr, Ipv4Addr};
use ipnet::Ipv4Net;
use netscli_core::DiscoverEngine;
fn loopback_slash_30() -> Ipv4Net {
"127.0.0.0/30".parse().expect("a valid /30")
}
#[tokio::test]
async fn a_small_subnet_scan_completes_and_is_bounded() {
let engine = DiscoverEngine::new_with_timeouts(8, 300, 300);
let started = std::time::Instant::now();
let hosts = engine
.scan_subnet(loopback_slash_30(), false)
.await
.expect("scanning a /30 must succeed");
let elapsed = started.elapsed();
assert!(
elapsed.as_secs() < 30,
"a /30 scan should not take {elapsed:?}"
);
for host in &hosts {
let IpAddr::V4(v4) = host.ip else {
panic!("an IPv4 subnet scan returned a non-IPv4 host: {host:?}");
};
assert!(
loopback_slash_30().contains(&v4),
"{v4} is outside the scanned range"
);
}
}
#[tokio::test]
async fn resolving_names_does_not_change_which_hosts_are_returned() {
let engine = DiscoverEngine::new_with_timeouts(8, 300, 300);
let without = engine
.scan_subnet(loopback_slash_30(), false)
.await
.unwrap();
let with = engine.scan_subnet(loopback_slash_30(), true).await.unwrap();
let mut a: Vec<IpAddr> = without.iter().map(|h| h.ip).collect();
let mut b: Vec<IpAddr> = with.iter().map(|h| h.ip).collect();
a.sort_unstable();
b.sort_unstable();
assert_eq!(a, b, "resolving names changed the set of hosts found");
}
#[tokio::test]
async fn a_hostname_is_never_blank_or_control_laden() {
let engine = DiscoverEngine::new_with_timeouts(8, 300, 300);
let hosts = engine.scan_subnet(loopback_slash_30(), true).await.unwrap();
for host in &hosts {
let Some(name) = host.hostname.as_deref() else {
continue;
};
assert!(!name.trim().is_empty(), "empty hostname on {}", host.ip);
assert!(
!name.chars().any(char::is_control),
"control character in hostname {name:?} for {}",
host.ip
);
}
}
#[tokio::test]
async fn the_engine_refuses_a_subnet_over_the_limit() {
let engine = DiscoverEngine::new_with_timeouts(8, 300, 300);
let error = engine
.scan_subnet("0.0.0.0/0".parse().unwrap(), false)
.await
.expect_err("an unbounded subnet must be refused");
assert!(
error.to_string().contains("subnet too large"),
"got: {error}"
);
}
#[tokio::test]
async fn hosts_serialize_with_the_fields_downstream_reads() {
let host = netscli_core::Host {
ip: IpAddr::V4(Ipv4Addr::new(192, 168, 1, 10)),
hostname: Some("device.local".to_string()),
mac: Some("00:11:22:33:44:55".to_string()),
vendor: Some("Example Corp".to_string()),
rtt_ms: Some(3),
found_by: netscli_core::FoundBy::Probe,
};
let value = serde_json::to_value(&host).expect("Host serializes");
for key in ["ip", "hostname", "mac", "vendor", "found_by"] {
assert!(value.get(key).is_some(), "missing {key}: {value}");
}
}
#[tokio::test]
async fn a_host_that_never_answers_is_still_reported_when_the_os_knows_it() {
let engine = DiscoverEngine::new_with_timeouts(8, 300, 300);
let hosts = engine
.scan_subnet(loopback_slash_30(), false)
.await
.unwrap();
for host in &hosts {
match host.found_by {
netscli_core::FoundBy::Probe => {}
netscli_core::FoundBy::Neighbor => assert!(
host.rtt_ms.is_none(),
"a neighbour-only host cannot have an RTT: {host:?}"
),
}
}
}