forked from Crockan/MercuryToolbox
259 lines
7.7 KiB
Rust
259 lines
7.7 KiB
Rust
//! Integration tests for the `portping` command.
|
|
|
|
use std::io::{BufRead, BufReader, Read, Write};
|
|
use std::net::{TcpListener, TcpStream};
|
|
use std::process::{Child, Command as StdCommand, Stdio};
|
|
use std::sync::mpsc;
|
|
use std::thread;
|
|
use std::time::{Duration, Instant};
|
|
|
|
use assert_cmd::Command;
|
|
use portping::{Target, parse_target};
|
|
use predicates::prelude::*;
|
|
|
|
fn cargo_command() -> Command {
|
|
Command::cargo_bin("portping").expect("binary")
|
|
}
|
|
|
|
fn spawn_http_server() -> u16 {
|
|
let listener = TcpListener::bind("127.0.0.1:0").expect("listener");
|
|
let port = listener.local_addr().expect("address").port();
|
|
|
|
thread::spawn(move || {
|
|
if let Ok((mut stream, _)) = listener.accept() {
|
|
let mut buffer = [0_u8; 1024];
|
|
let _ = stream.read(&mut buffer);
|
|
let response = b"HTTP/1.1 200 OK\r\nContent-Length: 2\r\nConnection: close\r\n\r\nok";
|
|
let _ = stream.write_all(response);
|
|
}
|
|
});
|
|
|
|
port
|
|
}
|
|
|
|
fn spawn_capturing_http_server(status_line: &'static str) -> (u16, mpsc::Receiver<String>) {
|
|
let listener = TcpListener::bind("127.0.0.1:0").expect("listener");
|
|
let port = listener.local_addr().expect("address").port();
|
|
let (sender, receiver) = mpsc::channel();
|
|
|
|
thread::spawn(move || {
|
|
if let Ok((mut stream, _)) = listener.accept() {
|
|
let mut buffer = [0_u8; 1024];
|
|
let read = stream.read(&mut buffer).expect("read request");
|
|
let request = String::from_utf8_lossy(&buffer[..read]).to_string();
|
|
sender.send(request).expect("send request");
|
|
let response =
|
|
format!("{status_line}\r\nContent-Length: 0\r\nConnection: close\r\n\r\n");
|
|
stream
|
|
.write_all(response.as_bytes())
|
|
.expect("write response");
|
|
}
|
|
});
|
|
|
|
(port, receiver)
|
|
}
|
|
|
|
fn unused_local_port() -> u16 {
|
|
let listener = TcpListener::bind("127.0.0.1:0").expect("listener");
|
|
let port = listener.local_addr().expect("address").port();
|
|
drop(listener);
|
|
port
|
|
}
|
|
|
|
struct ChildGuard(Child);
|
|
|
|
impl Drop for ChildGuard {
|
|
fn drop(&mut self) {
|
|
let _ = self.0.kill();
|
|
let _ = self.0.wait();
|
|
}
|
|
}
|
|
|
|
fn spawn_passive_listener_child(port: u16) -> ChildGuard {
|
|
let mut child = StdCommand::new("pwsh")
|
|
.arg("-NoProfile")
|
|
.arg("-Command")
|
|
.arg(format!(
|
|
"$listener = [System.Net.Sockets.TcpListener]::new([System.Net.IPAddress]::Loopback, {port}); \
|
|
$listener.Start(); \
|
|
Write-Output 'ready'; \
|
|
try {{ Start-Sleep -Seconds 30 }} finally {{ $listener.Stop() }}"
|
|
))
|
|
.stdout(Stdio::piped())
|
|
.spawn()
|
|
.expect("spawn passive listener child");
|
|
let stdout = child.stdout.take().expect("passive listener stdout");
|
|
let (sender, receiver) = mpsc::channel();
|
|
thread::spawn(move || {
|
|
let mut line = String::new();
|
|
let _ = BufReader::new(stdout).read_line(&mut line);
|
|
let _ = sender.send(line);
|
|
});
|
|
|
|
let ready_line = receiver
|
|
.recv_timeout(Duration::from_secs(15))
|
|
.expect("passive listener child did not report readiness");
|
|
assert!(
|
|
ready_line.trim() == "ready",
|
|
"unexpected passive listener readiness line: {ready_line:?}"
|
|
);
|
|
|
|
ChildGuard(child)
|
|
}
|
|
|
|
fn wait_for_tcp_listener(port: u16) {
|
|
let deadline = Instant::now() + Duration::from_secs(5);
|
|
while Instant::now() < deadline {
|
|
if TcpStream::connect(("127.0.0.1", port)).is_ok() {
|
|
return;
|
|
}
|
|
thread::sleep(Duration::from_millis(25));
|
|
}
|
|
panic!("passive listener on port {port} did not become ready");
|
|
}
|
|
|
|
#[test]
|
|
fn parses_targets() {
|
|
assert!(matches!(
|
|
parse_target("tcp://127.0.0.1:9000").expect("tcp"),
|
|
Target::Tcp { port, .. } if port == 9000
|
|
));
|
|
assert!(matches!(
|
|
parse_target("http://127.0.0.1:9000/health").expect("http"),
|
|
Target::Http { port, .. } if port == 9000
|
|
));
|
|
}
|
|
|
|
#[test]
|
|
fn probes_http_endpoint_as_json() {
|
|
let port = spawn_http_server();
|
|
let mut command = cargo_command();
|
|
command
|
|
.arg(format!("http://127.0.0.1:{port}/health"))
|
|
.arg("--json")
|
|
.assert()
|
|
.success()
|
|
.stdout(predicate::str::contains("\"status_code\":200"))
|
|
.stdout(predicate::str::contains("\"ok\":true"));
|
|
}
|
|
|
|
#[test]
|
|
fn reports_closed_tcp_port() {
|
|
let port = unused_local_port();
|
|
|
|
let mut command = cargo_command();
|
|
command
|
|
.arg(format!("tcp://127.0.0.1:{port}"))
|
|
.arg("--json")
|
|
.assert()
|
|
.code(1)
|
|
.stdout(predicate::str::contains("\"ok\":false"))
|
|
.stdout(predicate::str::contains("\"error\""));
|
|
}
|
|
|
|
#[test]
|
|
fn report_mode_keeps_diagnostic_sweeps_successful_when_probe_fails() {
|
|
let port = unused_local_port();
|
|
|
|
let mut command = cargo_command();
|
|
command
|
|
.arg(format!("tcp://127.0.0.1:{port}"))
|
|
.arg("--report")
|
|
.arg("--json")
|
|
.assert()
|
|
.success()
|
|
.stdout(predicate::str::contains("\"ok\":false"))
|
|
.stdout(predicate::str::contains("\"error_code\""));
|
|
}
|
|
|
|
#[test]
|
|
fn supports_powershell_pipeline() {
|
|
let port = spawn_http_server();
|
|
let binary = assert_cmd::cargo::cargo_bin("portping");
|
|
let script = format!(
|
|
"'http://127.0.0.1:{port}/health' | & '{}'",
|
|
binary.display()
|
|
);
|
|
|
|
let mut command = Command::new("pwsh");
|
|
command
|
|
.arg("-NoProfile")
|
|
.arg("-Command")
|
|
.arg(script)
|
|
.assert()
|
|
.success()
|
|
.stdout(predicate::str::contains("200"))
|
|
.stdout(predicate::str::contains("ok"));
|
|
}
|
|
|
|
#[test]
|
|
fn supports_head_requests_and_expected_status_checks() {
|
|
let (port, receiver) = spawn_capturing_http_server("HTTP/1.1 204 No Content");
|
|
let mut command = cargo_command();
|
|
command
|
|
.arg(format!("http://127.0.0.1:{port}/health"))
|
|
.arg("--method")
|
|
.arg("HEAD")
|
|
.arg("--expect-status")
|
|
.arg("204")
|
|
.arg("--json")
|
|
.assert()
|
|
.success()
|
|
.stdout(predicate::str::contains("\"method\":\"HEAD\""))
|
|
.stdout(predicate::str::contains("\"expected_status\":204"))
|
|
.stdout(predicate::str::contains(
|
|
"\"status_matches_expectation\":true",
|
|
))
|
|
.stdout(predicate::str::contains("\"ok\":true"));
|
|
|
|
let request = receiver.recv().expect("captured request");
|
|
assert!(request.starts_with("HEAD /health HTTP/1.1\r\n"));
|
|
}
|
|
|
|
#[test]
|
|
fn probes_passive_loopback_listener_started_by_child_process() {
|
|
let port = unused_local_port();
|
|
let _listener = spawn_passive_listener_child(port);
|
|
wait_for_tcp_listener(port);
|
|
|
|
let mut command = cargo_command();
|
|
command
|
|
.arg(format!("tcp://127.0.0.1:{port}"))
|
|
.arg("--json")
|
|
.assert()
|
|
.success()
|
|
.stdout(predicate::str::contains("\"ok\":true"));
|
|
}
|
|
|
|
#[test]
|
|
fn marks_status_mismatches_as_failed_results() {
|
|
let port = spawn_http_server();
|
|
let mut command = cargo_command();
|
|
command
|
|
.arg(format!("http://127.0.0.1:{port}/health"))
|
|
.arg("--expect-status")
|
|
.arg("204")
|
|
.arg("--json")
|
|
.assert()
|
|
.code(1)
|
|
.stdout(predicate::str::contains("\"status_code\":200"))
|
|
.stdout(predicate::str::contains(
|
|
"\"status_matches_expectation\":false",
|
|
))
|
|
.stdout(predicate::str::contains("\"ok\":false"));
|
|
}
|
|
|
|
#[test]
|
|
fn help_includes_health_check_examples() {
|
|
let mut command = cargo_command();
|
|
command
|
|
.arg("--help")
|
|
.assert()
|
|
.success()
|
|
.stdout(predicate::str::contains("--method"))
|
|
.stdout(predicate::str::contains("--expect-status"))
|
|
.stdout(predicate::str::contains("--report"))
|
|
.stdout(predicate::str::contains("Exit code"))
|
|
.stdout(predicate::str::contains("--toon"));
|
|
}
|