use std::net::{IpAddr, Ipv4Addr, Ipv6Addr};
use std::time::Instant;
use netscli_core::PingScanner;
#[tokio::test]
async fn a_ping_always_answers_with_a_well_formed_result() {
let scanner = PingScanner::new(1);
let target = IpAddr::V4(Ipv4Addr::LOCALHOST);
let result = scanner.ping(target, 1_000).await;
assert_eq!(
result.ip, target,
"the result must name the target it probed"
);
assert!(
result.method.is_some(),
"every result must say which mechanism produced it: {result:?}"
);
assert!(result.seq >= 1);
if result.alive {
assert!(result.rtt_ms.is_some(), "a live host must carry an RTT");
}
}
#[tokio::test]
async fn an_ipv6_target_is_handled_rather_than_panicking() {
let scanner = PingScanner::new(1);
let target = IpAddr::V6(Ipv6Addr::LOCALHOST);
let result = scanner.ping(target, 1_000).await;
assert_eq!(result.ip, target);
assert!(result.method.is_some());
}
#[tokio::test]
async fn the_timeout_is_honoured_for_an_unroutable_target() {
let scanner = PingScanner::new(1);
let target = IpAddr::V4(Ipv4Addr::new(192, 0, 2, 1));
let started = Instant::now();
let result = scanner.ping(target, 500).await;
let elapsed = started.elapsed();
assert_eq!(result.ip, target);
assert!(
elapsed.as_secs() < 20,
"ping ignored its timeout: took {elapsed:?}"
);
}
#[tokio::test]
async fn concurrency_zero_does_not_deadlock() {
let scanner = PingScanner::new(0);
let result = scanner.ping(IpAddr::V4(Ipv4Addr::LOCALHOST), 500).await;
assert_eq!(result.ip, IpAddr::V4(Ipv4Addr::LOCALHOST));
}
#[tokio::test]
async fn results_serialize_with_the_fields_downstream_reads() {
let scanner = PingScanner::new(1);
let result = scanner.ping(IpAddr::V4(Ipv4Addr::LOCALHOST), 1_000).await;
let value = serde_json::to_value(&result).expect("PingResult serializes");
for key in ["ip", "alive", "seq"] {
assert!(value.get(key).is_some(), "missing {key}: {value}");
}
}
#[cfg(windows)]
#[tokio::test]
async fn pinging_this_host_succeeds_on_windows() {
let scanner = PingScanner::new(1);
let target = IpAddr::V4(Ipv4Addr::LOCALHOST);
let result = scanner.ping(target, 2_000).await;
assert!(
result.alive,
"loopback must answer its own ping: {result:?}"
);
assert!(result.rtt_ms.is_some(), "a live host must carry an RTT");
assert_eq!(
result.method.as_deref(),
Some("icmpv4"),
"loopback should be reached over ICMP, not the TCP fallback: {result:?}"
);
}
#[cfg(windows)]
#[tokio::test]
async fn pinging_an_address_this_host_owns_succeeds_on_windows() {
let Some(local) = netscli_core::NetworkManager::get_interfaces()
.into_iter()
.filter(|iface| !iface.is_loopback && iface.is_up)
.find_map(|iface| {
iface.ips.into_iter().find_map(|net| match net.addr() {
IpAddr::V4(v4) if !v4.is_link_local() && !v4.is_loopback() => Some(IpAddr::V4(v4)),
_ => None,
})
})
else {
return;
};
let result = PingScanner::new(1).ping(local, 2_000).await;
assert!(
result.alive,
"this host must answer a ping to its own address {local}: {result:?}"
);
}
#[cfg(windows)]
#[tokio::test]
async fn pinging_ipv6_loopback_succeeds_on_windows() {
let scanner = PingScanner::new(1);
let target = IpAddr::V6(Ipv6Addr::LOCALHOST);
let result = scanner.ping(target, 2_000).await;
assert!(result.alive, "::1 must answer its own ping: {result:?}");
assert_eq!(
result.method.as_deref(),
Some("icmpv6"),
"v6 should go over ICMPv6, not the TCP fallback: {result:?}"
);
}
#[cfg(windows)]
#[tokio::test]
async fn an_unreachable_ipv6_target_is_reported_dead() {
let target = IpAddr::V6(Ipv6Addr::new(0x2001, 0xdb8, 0, 0, 0, 0, 0, 1));
let result = PingScanner::new(1).ping(target, 1_000).await;
assert!(
!result.alive,
"documentation space must not answer: {result:?}"
);
}