Files

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"));
}