use std::{
io::{Read, Write},
net::{TcpListener, TcpStream},
thread,
time::Duration,
};
struct MockServer {
listener: TcpListener,
}
impl MockServer {
fn new() -> Self {
let listener = TcpListener::bind("127.0.0.1:0").unwrap();
Self { listener }
}
fn port(&self) -> u16 {
self.listener.local_addr().unwrap().port()
}
fn handle_connection(mut stream: TcpStream) {
let mut buffer = [0u8; 1024];
stream.read(&mut buffer).unwrap();
let request = String::from_utf8_lossy(&buffer);
let (content_type, body) = if request.contains("POST") {
("application/json", "{\"status\":\"success\"}")
} else if request.contains("Authorization: Bearer token") {
("application/json", "{\"authenticated\":true}")
} else if request.contains("chunked") {
("text/plain", "Hello, Chunked World!")
} else {
("text/plain", "Hello, World!")
};
let response = format!(
"HTTP/1.1 200 OK\r\nContent-Type: {}\r\nContent-Length: {}\r\nConnection: close\r\n\r\n{}",
content_type,
body.len(),
body
);
stream.write_all(response.as_bytes()).unwrap();
stream.flush().unwrap();
}
fn run(&self) {
for stream in self.listener.incoming() {
match stream {
Ok(stream) => {
thread::spawn(move || {
Self::handle_connection(stream);
});
}
Err(e) => {
eprintln!("Error: {}", e);
}
}
}
}
}
#[test]
fn test_basic_get_request() {
let server = MockServer::new();
let port = server.port();
thread::spawn(move || server.run());
thread::sleep(Duration::from_millis(100));
let output = std::process::Command::new("cargo")
.args(["run", "--", &format!("http://127.0.0.1:{}", port)])
.output()
.unwrap();
assert!(output.status.success());
assert!(String::from_utf8_lossy(&output.stdout).contains("Hello, World!"));
}
#[test]
fn test_verbose_output() {
let server = MockServer::new();
let port = server.port();
thread::spawn(move || server.run());
thread::sleep(Duration::from_millis(100));
let output = std::process::Command::new("cargo")
.args(["run", "--", "-v", &format!("http://127.0.0.1:{}", port)])
.output()
.unwrap();
let stdout = String::from_utf8_lossy(&output.stdout);
assert!(output.status.success());
assert!(stdout.contains("Hello, World!"));
assert!(stdout.contains("Connecting to"));
assert!(stdout.contains("Sending request"));
assert!(stdout.contains("Content-Length"));
assert!(stdout.contains("Status: HTTP/1.1 200 OK"));
}
#[test]
fn test_save_to_file() {
let server = MockServer::new();
let port = server.port();
thread::spawn(move || server.run());
thread::sleep(Duration::from_millis(100));
let output_file = "test_output.txt";
let output = std::process::Command::new("cargo")
.args([
"run",
"--",
&format!("http://127.0.0.1:{}", port),
"-o",
output_file,
])
.output()
.unwrap();
assert!(output.status.success());
assert!(output.stdout.len() > 0);
let file_content = std::fs::read_to_string(output_file).unwrap();
assert!(file_content.contains("Hello, World!"));
std::fs::remove_file(output_file).unwrap();
}
#[test]
fn test_post_request() {
let server = MockServer::new();
let port = server.port();
thread::spawn(move || server.run());
thread::sleep(Duration::from_millis(100));
let output = std::process::Command::new("cargo")
.args([
"run",
"--",
&format!("http://127.0.0.1:{}", port),
"-m",
"POST",
"-d",
"{\"key\":\"value\"}",
])
.output()
.unwrap();
assert!(output.status.success());
assert!(String::from_utf8_lossy(&output.stdout).contains("success"));
}
#[test]
fn test_custom_headers() {
let server = MockServer::new();
let port = server.port();
thread::spawn(move || server.run());
thread::sleep(Duration::from_millis(100));
let output = std::process::Command::new("cargo")
.args([
"run",
"--",
&format!("http://127.0.0.1:{}", port),
"-H",
"Authorization: Bearer token",
"-H",
"Content-Type: application/json",
])
.output()
.unwrap();
assert!(output.status.success());
assert!(String::from_utf8_lossy(&output.stdout).contains("authenticated"));
}
#[test]
fn test_help_flag() {
let output = std::process::Command::new("cargo")
.args(["run", "--", "--help"])
.output()
.unwrap();
assert!(output.status.success());
let stdout = String::from_utf8_lossy(&output.stdout);
assert!(stdout.contains("rurl - A minimal HTTP client"));
assert!(stdout.contains("-v, --verbose"));
}
#[test]
fn test_invalid_url() {
let output = std::process::Command::new("cargo")
.args(["run", "--", "not-a-valid-url"])
.output()
.unwrap();
assert!(!output.status.success());
assert!(String::from_utf8_lossy(&output.stderr).contains("URL must start with http://"));
}
#[test]
fn test_malformed_url() {
let output = std::process::Command::new("cargo")
.args(["run", "--", "http://"])
.output()
.unwrap();
assert!(!output.status.success());
assert!(String::from_utf8_lossy(&output.stderr).contains("Invalid host"));
}
#[test]
fn test_invalid_port() {
let output = std::process::Command::new("cargo")
.args(["run", "--", "http://localhost:99999"])
.output()
.unwrap();
assert!(!output.status.success());
assert!(String::from_utf8_lossy(&output.stderr).contains("Invalid port"));
}
#[test]
fn test_missing_url() {
let output = std::process::Command::new("cargo")
.args(["run"])
.output()
.unwrap();
assert!(!output.status.success());
assert!(String::from_utf8_lossy(&output.stderr).contains("Missing URL"));
}
#[test]
fn test_connection_timeout() {
let output = std::process::Command::new("cargo")
.args(["run", "--", "http://192.168.255.255:12345"])
.output()
.unwrap();
assert!(!output.status.success());
let stderr = String::from_utf8_lossy(&output.stderr);
assert!(
stderr.contains("Connection") || stderr.contains("timeout") || stderr.contains("timed out"),
"Expected connection error or timeout, got: {}",
stderr
);
}
#[test]
fn test_tls_connection_attempt() {
use std::process::{Command, Stdio};
use std::thread;
use std::time::Duration;
let mut child = Command::new("cargo")
.args(["run", "--", "https://example.com"])
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.spawn()
.unwrap();
thread::sleep(Duration::from_secs(5));
match child.try_wait() {
Ok(Some(status)) => {
let output = child.wait_with_output().unwrap();
if status.success() {
let stdout = String::from_utf8_lossy(&output.stdout);
assert!(
stdout.contains("<html")
|| stdout.contains("<body")
|| stdout.contains("<!DOCTYPE"),
"Expected HTML response, got: {}",
stdout
);
} else {
let stderr = String::from_utf8_lossy(&output.stderr);
assert!(
stderr.contains("TLS")
|| stderr.contains("SSL")
|| stderr.contains("handshake")
|| stderr.contains("Connection"),
"Expected TLS error, got: {}",
stderr
);
}
}
Ok(None) => {
let _ = child.kill();
let _ = child.wait();
panic!("Test timed out - request is taking too long to complete");
}
Err(e) => panic!("Error waiting for process: {}", e),
}
}
#[test]
fn test_invalid_tls_hostname() {
let output = std::process::Command::new("cargo")
.args([
"run",
"--",
"https://invalid-hostname-that-doesnt-exist.example",
])
.output()
.unwrap();
assert!(!output.status.success());
let stderr = String::from_utf8_lossy(&output.stderr);
assert!(
stderr.contains("DNS")
|| stderr.contains("TLS")
|| stderr.contains("not found")
|| stderr.contains("unknown")
|| stderr.contains("Connection")
|| stderr.contains("connect"),
"Expected connection error, got: {}",
stderr
);
}