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