use specter::Client;
use std::time::Duration;
use tokio::io::AsyncWriteExt;
use tokio::net::TcpListener;
mod helpers;
#[tokio::test]
async fn test_connection_refused() {
let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
let port = listener.local_addr().unwrap().port();
drop(listener);
let client = Client::builder()
.prefer_http2(false)
.build()
.unwrap();
let url = format!("http://127.0.0.1:{}/test", port);
let result = client.get(url.as_str()).send().await;
assert!(result.is_err(), "Expected connection refused error");
let err = result.unwrap_err();
let err_msg = format!("{}", err);
assert!(
err_msg.contains("Connection") || err_msg.contains("IO") || err_msg.contains("refused"),
"Expected connection-related error, got: {}",
err_msg
);
}
#[tokio::test]
async fn test_dns_failure() {
let client = Client::builder()
.prefer_http2(false)
.connect_timeout(Duration::from_secs(2))
.build()
.unwrap();
let url = "http://this-host-does-not-exist-xyzzy-12345.invalid/test";
let result = client.get(url).send().await;
assert!(result.is_err(), "Expected DNS resolution error");
let err = result.unwrap_err();
let err_msg = format!("{}", err);
assert!(
err_msg.contains("IO")
|| err_msg.contains("Connection")
|| err_msg.contains("dns")
|| err_msg.contains("resolve")
|| err_msg.contains("No address")
|| err_msg.contains("not known")
|| err_msg.contains("nodename nor servname"),
"Expected DNS-related error, got: {}",
err_msg
);
}
#[tokio::test]
async fn test_read_timeout_ttfb() {
let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
let port = listener.local_addr().unwrap().port();
tokio::spawn(async move {
loop {
let (stream, _) = listener.accept().await.unwrap();
tokio::spawn(async move {
let _held = stream;
tokio::time::sleep(Duration::from_secs(3600)).await;
});
}
});
let client = Client::builder()
.prefer_http2(false)
.ttfb_timeout(Duration::from_millis(200))
.total_timeout(Duration::from_millis(500))
.build()
.unwrap();
let url = format!("http://127.0.0.1:{}/test", port);
let result = client.get(url.as_str()).send().await;
assert!(result.is_err(), "Expected timeout error");
let err_msg = format!("{}", result.unwrap_err());
assert!(
err_msg.contains("timeout") || err_msg.contains("Timeout") || err_msg.contains("timed out"),
"Expected timeout-related error, got: {}",
err_msg
);
}
#[tokio::test]
async fn test_connection_reset_partial_response() {
let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
let port = listener.local_addr().unwrap().port();
tokio::spawn(async move {
let (mut stream, _) = listener.accept().await.unwrap();
let mut buf = [0u8; 4096];
let _ = tokio::io::AsyncReadExt::read(&mut stream, &mut buf).await;
let partial = b"HTTP/1.1 200 OK\r\nContent-Length: 1000\r\nConnection: close\r\n\r\nPartial";
let _ = stream.write_all(partial).await;
let _ = stream.flush().await;
drop(stream);
});
tokio::time::sleep(Duration::from_millis(50)).await;
let client = Client::builder()
.prefer_http2(false)
.build()
.unwrap();
let url = format!("http://127.0.0.1:{}/test", port);
let result = client.get(url.as_str()).send().await;
match result {
Ok(resp) => {
let body = resp.body();
assert!(
body.len() < 1000,
"Expected truncated body (got {} bytes)",
body.len()
);
}
Err(e) => {
let err_msg = format!("{}", e);
assert!(
err_msg.contains("IO")
|| err_msg.contains("Connection")
|| err_msg.contains("reset")
|| err_msg.contains("closed")
|| err_msg.contains("incomplete")
|| err_msg.contains("unexpected eof")
|| err_msg.contains("protocol"),
"Expected connection/IO error, got: {}",
err_msg
);
}
}
}
#[tokio::test]
async fn test_tls_handshake_failure() {
let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
let port = listener.local_addr().unwrap().port();
tokio::spawn(async move {
loop {
match listener.accept().await {
Ok((mut stream, _)) => {
let _ = stream.write_all(b"This is not TLS").await;
drop(stream);
}
Err(_) => break,
}
}
});
tokio::time::sleep(Duration::from_millis(50)).await;
let client = Client::builder()
.localhost_allows_invalid_certs(false)
.connect_timeout(Duration::from_secs(5))
.build()
.unwrap();
let url = format!("https://127.0.0.1:{}/test", port);
let result = client.get(url.as_str()).send().await;
assert!(result.is_err(), "Expected TLS handshake error");
let err_msg = format!("{}", result.unwrap_err());
assert!(
err_msg.contains("TLS")
|| err_msg.contains("tls")
|| err_msg.contains("SSL")
|| err_msg.contains("ssl")
|| err_msg.contains("handshake")
|| err_msg.contains("IO")
|| err_msg.contains("Connection"),
"Expected TLS-related error, got: {}",
err_msg
);
}
#[tokio::test]
async fn test_combined_timeouts() {
let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
let port = listener.local_addr().unwrap().port();
tokio::spawn(async move {
loop {
let (stream, _) = listener.accept().await.unwrap();
tokio::spawn(async move {
let _held = stream;
tokio::time::sleep(Duration::from_secs(3600)).await;
});
}
});
let client = Client::builder()
.prefer_http2(false)
.connect_timeout(Duration::from_secs(5))
.ttfb_timeout(Duration::from_millis(200))
.build()
.unwrap();
let url = format!("http://127.0.0.1:{}/test", port);
let result = client.get(url.as_str()).send().await;
assert!(result.is_err(), "Expected TTFB timeout error");
let err_msg = format!("{}", result.unwrap_err());
assert!(
err_msg.contains("timeout") || err_msg.contains("Timeout") || err_msg.contains("TTFB"),
"Expected timeout-related error, got: {}",
err_msg
);
}