use std::path::{Path, PathBuf};
use std::process::{Command, Output};
fn config_home(test: &str) -> PathBuf {
let directory = PathBuf::from(env!("CARGO_TARGET_TMPDIR")).join(test);
let _ = std::fs::remove_dir_all(&directory);
std::fs::create_dir_all(&directory).expect("a writable target directory");
directory
}
fn zond_in(directory: &Path, args: &[&str]) -> Output {
Command::new(env!("CARGO_BIN_EXE_zond"))
.args(args)
.env("XDG_CONFIG_HOME", directory)
.output()
.expect("the binary under test should run")
}
fn zond(test: &str, args: &[&str]) -> Output {
zond_in(&config_home(test), args)
}
fn stdout(output: &Output) -> String {
String::from_utf8_lossy(&output.stdout).into_owned()
}
fn untimed(fields: &[String]) -> Vec<String> {
fields
.iter()
.enumerate()
.filter(|(index, _)| !matches!(index, 2 | 8 | 9 | 10))
.map(|(_, field)| field.clone())
.collect()
}
fn record(output: &Output) -> Vec<String> {
let text = stdout(output);
let line = text.lines().next().unwrap_or_else(|| {
panic!(
"expected one record, got nothing. stderr:\n{}",
stderr(output)
)
});
line.split('\t').map(str::to_owned).collect()
}
fn status(output: &Output) -> i32 {
output
.status
.code()
.expect("the run should not be signalled")
}
fn stderr(output: &Output) -> String {
String::from_utf8_lossy(&output.stderr).into_owned()
}
#[test]
fn a_loopback_sweep_runs_to_completion() {
let run = zond("loopback", &["-q", "d", "127.0.0.1"]);
assert_eq!(status(&run), 0, "{}", stderr(&run));
}
#[test]
fn an_ipv6_target_runs_the_same_way() {
let run = zond("ipv6", &["-q", "d", "::1"]);
assert_eq!(status(&run), 0, "{}", stderr(&run));
}
#[test]
fn an_empty_result_is_still_a_success() {
let run = zond("empty", &["-q", "d", "192.0.2.1-4"]);
assert_eq!(status(&run), 0, "{}", stderr(&run));
}
#[test]
fn a_malformed_target_is_a_usage_error() {
let run = zond("malformed", &["d", "192.168.0.300"]);
assert_eq!(status(&run), 2, "{}", stderr(&run));
}
#[test]
fn an_ipv4_range_too_large_to_sweep_is_a_usage_error() {
let run = zond("huge-v4", &["d", "10.0.0.0/8"]);
assert_eq!(status(&run), 2, "{}", stderr(&run));
assert!(stderr(&run).contains("16777216"), "{}", stderr(&run));
}
#[test]
fn an_unwalkable_ipv6_range_is_refused_by_the_engine_and_reported_partial() {
let run = zond("unwalkable-v6", &["-q", "d", "2001:db8::/64"]);
assert_eq!(status(&run), 3, "{}", stderr(&run));
}
#[test]
fn a_hostname_is_refused_when_dns_is_forbidden() {
let run = zond("no-dns", &["-q", "d", "-n", "one.one.one.one"]);
assert_eq!(status(&run), 2, "{}", stderr(&run));
assert!(stderr(&run).contains("--no-dns"), "{}", stderr(&run));
}
#[test]
fn a_first_run_provisions_both_settings_files() {
let directory = config_home("provision");
let run = zond_in(&directory, &["-q", "d", "127.0.0.1"]);
assert_eq!(status(&run), 0, "{}", stderr(&run));
assert!(directory.join("zond/cli.toml").is_file());
assert!(directory.join("zond/engine.toml").is_file());
}
#[test]
fn provisioned_files_change_nothing_about_the_next_run() {
let directory = config_home("provision-inert");
let args = ["-q", "--pipe", "s", "127.0.0.1", "-p", "1,2"];
let first = zond_in(&directory, &args);
let second = zond_in(&directory, &args);
assert_eq!(status(&first), 0, "{}", stderr(&first));
assert_eq!(status(&second), status(&first));
assert_eq!(
untimed(&record(&second)),
untimed(&record(&first)),
"the second run saw the files the first one created"
);
}
#[test]
fn a_presentation_that_is_not_built_is_a_usage_error() {
let run = zond("fancy", &["--presentation", "fancy", "d", "127.0.0.1"]);
assert_eq!(status(&run), 2, "{}", stderr(&run));
assert!(stderr(&run).contains("not built yet"), "{}", stderr(&run));
}
#[test]
fn an_unusable_settings_file_is_a_usage_error() {
let directory = config_home("bad-settings");
std::fs::create_dir_all(directory.join("zond")).expect("a writable directory");
std::fs::write(
directory.join("zond/cli.toml"),
"presentation = \"shiny\"\n",
)
.expect("a writable file");
let run = zond_in(&directory, &["d", "127.0.0.1"]);
assert_eq!(status(&run), 2, "{}", stderr(&run));
assert!(stderr(&run).contains("shiny"), "{}", stderr(&run));
}
#[test]
fn an_unknown_setting_is_a_warning_and_the_run_continues() {
let directory = config_home("unknown-setting");
std::fs::create_dir_all(directory.join("zond")).expect("a writable directory");
std::fs::write(directory.join("zond/cli.toml"), "colour = true\n").expect("a writable file");
let run = zond_in(&directory, &["d", "127.0.0.1"]);
assert_eq!(status(&run), 0, "{}", stderr(&run));
assert!(stderr(&run).contains("colour"), "{}", stderr(&run));
}
#[test]
fn the_engines_default_ports_decide_a_scan_with_no_flag() {
let directory = config_home("engine-default-ports");
std::fs::create_dir_all(directory.join("zond")).expect("a writable directory");
std::fs::write(
directory.join("zond/engine.toml"),
"[defaults]\ndefault_ports = \"9001,9002\"\n",
)
.expect("a writable file");
let from_file = zond_in(&directory, &["s", "192.0.2.1"]);
assert!(
stderr(&from_file).contains("scanning 2 probes across 1 host"),
"{}",
stderr(&from_file)
);
let from_flag = zond_in(&directory, &["s", "192.0.2.1", "-p", "1-5"]);
assert!(
stderr(&from_flag).contains("scanning 5 probes across 1 host"),
"the flag must win outright, not merge: {}",
stderr(&from_flag)
);
}
#[test]
fn an_unusable_default_ports_is_refused_by_either_subcommand() {
let directory = config_home("bad-default-ports");
std::fs::create_dir_all(directory.join("zond")).expect("a writable directory");
std::fs::write(
directory.join("zond/engine.toml"),
"[defaults]\ndefault_ports = \"not-ports\"\n",
)
.expect("a writable file");
for subcommand in ["d", "s"] {
let run = zond_in(&directory, &["-q", subcommand, "127.0.0.1"]);
assert_eq!(status(&run), 2, "{subcommand}: {}", stderr(&run));
assert!(
stderr(&run).contains("default_ports"),
"{subcommand}: {}",
stderr(&run)
);
}
}
#[test]
fn the_engines_settings_file_is_honoured() {
let directory = config_home("engine-settings");
std::fs::create_dir_all(directory.join("zond")).expect("a writable directory");
std::fs::write(
directory.join("zond/engine.toml"),
"[defaults]\nno_dns = true\n",
)
.expect("a writable file");
let run = zond_in(&directory, &["-q", "d", "one.one.one.one"]);
assert_eq!(status(&run), 2, "{}", stderr(&run));
assert!(stderr(&run).contains("engine.toml"), "{}", stderr(&run));
}
#[test]
fn a_port_scan_runs_to_completion() {
let run = zond("scan-loopback", &["-q", "s", "127.0.0.1", "-p", "1,2,3"]);
assert_eq!(status(&run), 0, "{}", stderr(&run));
}
#[test]
fn the_ports_flag_decides_how_many_probes_are_spent() {
let one = zond("scan-probes-one", &["s", "192.0.2.1", "-p", "22,80,443"]);
assert!(
stderr(&one).contains("scanning 3 probes across 1 host"),
"{}",
stderr(&one)
);
let many = zond("scan-probes-many", &["s", "192.0.2.1-4", "-p", "1-10"]);
assert!(
stderr(&many).contains("scanning 40 probes across 4 hosts"),
"{}",
stderr(&many)
);
}
#[test]
fn a_scan_beyond_the_probe_limit_is_a_usage_error() {
let run = zond("scan-too-many", &["s", "10.0.0.0/16", "-p", "1-1024"]);
assert_eq!(status(&run), 2, "{}", stderr(&run));
assert!(stderr(&run).contains("67108864"), "{}", stderr(&run));
assert!(stderr(&run).contains("4194304"), "{}", stderr(&run));
}
#[test]
fn a_technique_that_needs_root_is_refused_rather_than_downgraded() {
let run = zond(
"scan-technique",
&[
"--pipe",
"s",
"127.0.0.1",
"-p",
"22",
"--tcp-technique",
"fin",
],
);
assert_eq!(status(&run), 3, "{}", stderr(&run));
assert!(stderr(&run).contains("raw sockets"), "{}", stderr(&run));
let fields = record(&run);
assert_eq!(fields[1], "Up", "the liveness phase still ran");
assert_eq!(fields[11], "-", "no port was probed");
assert_eq!(fields[12], "-", "and none was counted closed");
}
#[test]
fn the_pipe_format_carries_every_documented_field() {
let run = zond(
"scan-pipe",
&["-q", "--pipe", "s", "127.0.0.1", "-p", "1,2,3"],
);
assert_eq!(status(&run), 0, "{}", stderr(&run));
let fields = record(&run);
assert_eq!(fields.len(), 13, "{fields:?}");
assert_eq!(fields[0], "127.0.0.1");
assert_eq!(fields[1], "Up", "a reset proves the host is there");
let closed: usize = fields[12]
.parse()
.unwrap_or_else(|_| panic!("CLOSED should be a count, got {:?}", fields[12]));
assert!(
closed <= 3,
"more ports came back than were probed: {closed}"
);
}
#[test]
fn an_address_nothing_answers_for_is_not_port_scanned() {
let run = zond("scan-dead", &["--pipe", "s", "192.0.2.1", "-p", "1-64"]);
assert_eq!(status(&run), 0, "{}", stderr(&run));
assert_eq!(stdout(&run), "", "a dead address produced port records");
assert!(
stderr(&run).contains("--assume-up"),
"the note has to name the flag that overrides it: {}",
stderr(&run)
);
}
#[test]
fn assume_up_scans_an_address_that_answered_nothing() {
let run = zond(
"scan-assume-up",
&["-q", "--pipe", "s", "192.0.2.1", "-p", "1,2", "--assume-up"],
);
assert_eq!(status(&run), 0, "{}", stderr(&run));
let fields = record(&run);
assert_eq!(fields[0], "192.0.2.1");
assert!(
fields[11].contains("1/tcp") && fields[11].contains("2/tcp"),
"both ports should have been probed on trust: {:?}",
fields[11]
);
}
#[test]
fn a_live_host_is_scanned_and_timed() {
let run = zond(
"scan-live-timed",
&["-q", "--pipe", "s", "127.0.0.1", "-p", "1,2"],
);
let fields = record(&run);
assert_eq!(fields[1], "Up");
assert_ne!(fields[2], "-", "the liveness phase measured a round trip");
assert_eq!(
fields[12], "2",
"both ports were probed and came back closed"
);
}
#[test]
fn redaction_masks_what_identifies_a_host() {
let args = ["-q", "--pipe", "s", "::1", "-p", "1,2"];
let plain = zond("scan-plain", &args);
assert_eq!(status(&plain), 0, "{}", stderr(&plain));
let plain = record(&plain);
let mut redacted_args = args.to_vec();
redacted_args.push("--redact");
let redacted = zond("scan-redacted", &redacted_args);
assert_eq!(status(&redacted), 0, "{}", stderr(&redacted));
let redacted = record(&redacted);
assert_eq!(plain[0], "::1");
assert_ne!(redacted[0], "::1", "the address survived --redact");
if plain[5] != "-" {
assert_ne!(redacted[5], plain[5], "the name survived --redact");
}
}