use std::net::{IpAddr, Ipv4Addr};
use netscli_core::{PortScanner, PortStatus};
use tokio::net::TcpListener;
const LOCALHOST: IpAddr = IpAddr::V4(Ipv4Addr::LOCALHOST);
async fn open_port() -> (TcpListener, u16) {
let listener = TcpListener::bind((Ipv4Addr::LOCALHOST, 0))
.await
.expect("bind a loopback listener");
let port = listener.local_addr().expect("listener address").port();
(listener, port)
}
async fn closed_port() -> u16 {
let (listener, port) = open_port().await;
drop(listener);
port
}
#[tokio::test]
async fn an_open_port_is_reported_open() {
let (_listener, port) = open_port().await;
let scanner = PortScanner::new(8);
let results = scanner
.scan_host(LOCALHOST, vec![port], 2_000)
.await
.expect("scanning a bound loopback port must succeed");
assert_eq!(results.len(), 1);
let result = &results[0];
assert_eq!(result.port, port);
assert!(result.open, "a bound port must read as open: {result:?}");
assert_eq!(result.status, PortStatus::Open);
}
#[tokio::test]
async fn a_closed_port_is_reported_but_not_open() {
let port = closed_port().await;
let scanner = PortScanner::new(8);
let results = scanner
.scan_host(LOCALHOST, vec![port], 2_000)
.await
.expect("scanning an unbound loopback port must still succeed");
assert_eq!(results.len(), 1);
assert_eq!(results[0].port, port);
assert!(!results[0].open);
assert_ne!(results[0].status, PortStatus::Open);
}
#[tokio::test]
async fn every_requested_port_comes_back() {
let (_listener, open) = open_port().await;
let mut requested = vec![open];
for _ in 0..15 {
requested.push(closed_port().await);
}
let scanner = PortScanner::new(16);
let results = scanner
.scan_host(LOCALHOST, requested.clone(), 2_000)
.await
.expect("mixed open/closed scan must succeed");
assert_eq!(
results.len(),
requested.len(),
"every requested port must produce a result"
);
let mut got: Vec<u16> = results.iter().map(|r| r.port).collect();
let mut want = requested;
got.sort_unstable();
want.sort_unstable();
assert_eq!(got, want);
}
#[tokio::test]
async fn the_scanner_validates_its_own_port_list() {
let scanner = PortScanner::new(4);
assert!(
scanner.scan_host(LOCALHOST, vec![0], 500).await.is_err(),
"port 0 must be refused by the scanner itself"
);
let too_many: Vec<u16> = (1..=(netscli_core::MAX_PORTS_PER_SCAN as u16 + 1)).collect();
assert!(
scanner.scan_host(LOCALHOST, too_many, 500).await.is_err(),
"a list over MAX_PORTS_PER_SCAN must be refused"
);
}
#[tokio::test]
async fn results_serialize_with_the_fields_downstream_reads() {
let (_listener, port) = open_port().await;
let scanner = PortScanner::new(4);
let results = scanner
.scan_host(LOCALHOST, vec![port], 2_000)
.await
.unwrap();
let value = serde_json::to_value(&results[0]).expect("PortResult serializes");
assert!(value.get("port").is_some(), "port field: {value}");
assert!(value.get("open").is_some(), "open field: {value}");
assert!(value.get("status").is_some(), "status field: {value}");
}