sysreq 0.1.7

Simple, virtually-zero-dependencies HTTP client wrapping a system client
Documentation
use std::time::Duration;

#[cfg(test)]
use crate::SystemHttpClientInterface;

const TIMEOUT: Option<Duration> = Some(Duration::from_secs(30));

#[test]
fn test_get() {
	let reqwest = reqwest::blocking::Client::new();

	for test_url in [
		"https://www.google.com/favicon.ico",
		"http://www.example.org",
		"http://google.com/favicon.ico",
	] {
		for client in crate::clients::all_http_clients() {
			println!("{test_url} sysreq...");
			let result = client.get(test_url, TIMEOUT).unwrap();

			println!("{test_url} reqwest...");
			let truth = reqwest.get(test_url).send().unwrap().bytes().unwrap();

			println!("{test_url} checking...");

			if truth != result.body {
				let example_str = std::str::from_utf8(truth.as_ref());
				let result_str = std::str::from_utf8(result.body.as_ref());
				let diff = example_str
					.and_then(|truth_str| result_str.map(|result_str| difference::Changeset::new(truth_str, result_str, "")))
					.ok();
				panic!(
					"Client: {client:?}\nURL: {test_url}\n\nDiff:\nTruth: {} bytes vs Ours: {} bytes\n{}",
					truth.len(),
					result.body.len(),
					if let Some(diff) = diff {
						diff.to_string()
					} else {
						"<binary data>".to_string()
					},
				);
			}
		}
	}
}

#[test]
fn test_naughty_url() {
	match super::get("file:///etc/passwd") {
		Ok(_) => panic!("pwned"),
		Err(super::Error::InvalidUrlScheme) => {}
		Err(err) => panic!("{err}"),
	}
}

#[test]
fn test_str_interp_url() {
	std::env::set_var("SYSREQ_PWNED", "http://example.org");

	for client in crate::clients::all_http_clients() {
		for interp in ["$SYSREQ_PWNED", "`SYSREQ_PWNED`", "${SYSREQ_PWNED}", "[[SYSREQ_PWNED]]"].into_iter() {
			if let Ok(result) = client.get(interp, TIMEOUT) {
				if !result.body.is_empty() {
					panic!("This should have failed: {}", String::from_utf8_lossy(&result.body));
				}
			}
		}

		if let Ok(result) = client.get("#//\"\"\"\"\"'''''[[]]`````${hello}$hello###", TIMEOUT) {
			if !result.body.is_empty() {
				panic!("This should have failed: {}", String::from_utf8_lossy(&result.body));
			}
		}

		let example = client.get("http://example.org", TIMEOUT).unwrap();
		let result = client
			.get("http://example.org/#//\"\"\"\"\"'''''[[]]`````${hello}$hello###", TIMEOUT)
			.unwrap();
		if example.body != result.body {
			let example = String::from_utf8_lossy(&example.body);
			let result = String::from_utf8_lossy(&result.body);
			panic!("Diff:\n{}", difference::Changeset::new(example.as_ref(), result.as_ref(), ""));
		}
	}
}

#[test]
#[should_panic]
fn test_timeout_zero() {
	let _ = crate::RequestBuilder::new("http://localhost").timeout(Some(Duration::ZERO));
}

#[test]
fn test_timeouts() {
	let (tx, rx) = std::sync::mpsc::sync_channel(1);

	std::thread::spawn(move || {
		let server = std::net::TcpListener::bind("127.0.0.1:0").unwrap();

		tx.send(server.local_addr().unwrap()).unwrap();

		for client in server.incoming() {
			std::mem::forget(client);
		}
	});

	let url = format!("http://{}", rx.recv().unwrap());

	println!("Testing timeouts on {url}");

	for client in crate::clients::all_http_clients() {
		println!("Testing timeout for client {client:?}");
		let result = client.get(&url, Some(Duration::from_secs_f64(0.1)));
		match result {
			Err(crate::Error::IoError(err)) if err.kind() == std::io::ErrorKind::TimedOut => {}
			_ => panic!("{client:?}: {result:?}"),
		}
	}
}