use super::*;
use crate::runtime::tokio_rt::{TcpConnector, TokioRuntime};
use http::StatusCode;
use http_body_util::BodyExt;
fn test_client() -> HttpEngineSend<TokioRuntime, TcpConnector> {
HttpEngineSend::new()
}
#[tokio::test]
async fn header_sets_value() {
let client = test_client();
let rb = client.get("http://example.com").unwrap();
let rb = rb.header(http::header::ACCEPT, HeaderValue::from_static("text/html"));
let req = rb.build().unwrap();
assert_eq!(req.headers().get("accept").unwrap(), "text/html");
}
#[tokio::test]
async fn builder_read_accessors() {
let client = test_client();
let rb = client
.post("http://example.com/path?q=1")
.unwrap()
.header(http::header::ACCEPT, HeaderValue::from_static("text/html"));
assert_eq!(rb.method_ref(), &http::Method::POST);
assert_eq!(rb.url().to_string(), "http://example.com/path?q=1");
assert_eq!(
rb.headers_ref().get(http::header::ACCEPT).unwrap(),
"text/html"
);
}
#[tokio::test]
async fn headers_extends() {
let client = test_client();
let rb = client.get("http://example.com").unwrap();
let mut hm = HeaderMap::new();
hm.insert(
http::header::ACCEPT,
HeaderValue::from_static("application/json"),
);
hm.insert(
http::header::CACHE_CONTROL,
HeaderValue::from_static("no-cache"),
);
let rb = rb.headers(hm);
let req = rb.build().unwrap();
assert!(req.headers().contains_key("accept"));
assert!(req.headers().contains_key("cache-control"));
}
#[tokio::test]
async fn header_str_valid() {
let client = test_client();
let rb = client.get("http://example.com").unwrap();
let rb = rb.header_str("x-custom", "value").unwrap();
let req = rb.build().unwrap();
assert_eq!(req.headers().get("x-custom").unwrap(), "value");
}
#[tokio::test]
async fn bearer_auth_sets_authorization() {
let client = test_client();
let rb = client.get("http://example.com").unwrap();
let rb = rb.bearer_auth("mytoken");
let req = rb.build().unwrap();
let auth = req
.headers()
.get("authorization")
.unwrap()
.to_str()
.unwrap();
assert!(auth.starts_with("Bearer "));
assert!(auth.contains("mytoken"));
}
#[tokio::test]
async fn basic_auth_with_password() {
let client = test_client();
let rb = client.get("http://example.com").unwrap();
let rb = rb.basic_auth("user", Some("pass"));
let req = rb.build().unwrap();
let auth = req
.headers()
.get("authorization")
.unwrap()
.to_str()
.unwrap();
assert!(auth.starts_with("Basic "));
}
#[tokio::test]
async fn basic_auth_without_password() {
let client = test_client();
let rb = client.get("http://example.com").unwrap();
let rb = rb.basic_auth("user", None);
let req = rb.build().unwrap();
assert!(req.headers().contains_key("authorization"));
}
#[tokio::test]
async fn query_appends_params() {
let client = test_client();
let rb = client.get("http://example.com/path").unwrap();
let rb = rb.query(&[("key", "value"), ("a", "b")]);
let req = rb.build().unwrap();
let uri = req.uri().to_string();
assert!(uri.contains("key=value"));
assert!(uri.contains("a=b"));
}
#[tokio::test]
async fn query_appends_to_existing() {
let client = test_client();
let rb = client.get("http://example.com/path?existing=1").unwrap();
let rb = rb.query(&[("new", "2")]);
let req = rb.build().unwrap();
let uri = req.uri().to_string();
assert!(uri.contains("existing=1"));
assert!(uri.contains("new=2"));
}
#[tokio::test]
async fn body_sets_buffered() {
let client = test_client();
let rb = client.post("http://example.com").unwrap();
let rb = rb.body("hello");
let req = rb.build().unwrap();
match req.into_body() {
RequestBody::Buffered(b) => assert_eq!(b, "hello"),
_ => panic!("expected buffered"),
}
}
#[cfg(feature = "json")]
#[tokio::test]
async fn json_sets_content_type_and_body() {
let client = test_client();
let rb = client.post("http://example.com").unwrap();
let rb = rb.json(&serde_json::json!({"key": "value"})).unwrap();
let req = rb.build().unwrap();
assert_eq!(
req.headers().get("content-type").unwrap(),
"application/json"
);
}
#[cfg(feature = "json")]
#[tokio::test]
async fn json_preserves_existing_content_type() {
let client = test_client();
let rb = client.post("http://example.com").unwrap();
let rb = rb
.header(
http::header::CONTENT_TYPE,
HeaderValue::from_static("application/vnd.api+json"),
)
.json(&serde_json::json!({"key": "value"}))
.unwrap();
let req = rb.build().unwrap();
assert_eq!(
req.headers().get("content-type").unwrap(),
"application/vnd.api+json"
);
}
#[tokio::test]
async fn form_sets_content_type_and_body() {
let client = test_client();
let rb = client.post("http://example.com").unwrap();
let rb = rb.form(&[("a", "1"), ("b", "2")]);
let req = rb.build().unwrap();
assert_eq!(
req.headers().get("content-type").unwrap(),
"application/x-www-form-urlencoded"
);
match req.into_body() {
RequestBody::Buffered(b) => {
let s = String::from_utf8(b.to_vec()).unwrap();
assert!(s.contains("a=1"));
assert!(s.contains("b=2"));
}
_ => panic!("expected buffered"),
}
}
#[cfg(feature = "json")]
#[tokio::test]
async fn query_serde_appends_params() {
#[derive(serde::Serialize)]
struct Params {
key: String,
num: i32,
}
let client = test_client();
let rb = client.get("http://example.com/").unwrap();
let rb = rb
.query_serde(&Params {
key: "val".into(),
num: 42,
})
.unwrap();
let req = rb.build().unwrap();
let uri = req.uri().to_string();
assert!(uri.contains("key=val"));
assert!(uri.contains("num=42"));
}
#[cfg(feature = "json")]
#[tokio::test]
async fn form_serde_sets_body() {
#[derive(serde::Serialize)]
struct FormData {
name: String,
}
let client = test_client();
let rb = client.post("http://example.com").unwrap();
let rb = rb
.form_serde(&FormData {
name: "test".into(),
})
.unwrap();
let req = rb.build().unwrap();
assert_eq!(
req.headers().get("content-type").unwrap(),
"application/x-www-form-urlencoded"
);
}
#[tokio::test]
async fn version_sets_http_version() {
let client = test_client();
let rb = client.get("http://example.com").unwrap();
let rb = rb.version(Version::HTTP_11);
let req = rb.build().unwrap();
assert_eq!(req.version(), Version::HTTP_11);
}
#[tokio::test]
async fn build_default_body() {
let client = test_client();
let rb = client.get("http://example.com").unwrap();
let req = rb.build().unwrap();
assert_eq!(*req.method(), Method::GET);
}
#[tokio::test]
async fn try_clone_buffered() {
let client = test_client();
let rb = client.post("http://example.com").unwrap().body("data");
let cloned = rb.try_clone();
assert!(cloned.is_some());
}
#[tokio::test]
async fn try_clone_no_body() {
let client = test_client();
let rb = client.get("http://example.com").unwrap();
let cloned = rb.try_clone();
assert!(cloned.is_some());
}
#[tokio::test]
async fn try_clone_streaming_returns_none() {
use http_body_util::BodyExt;
let client = test_client();
let rb = client.post("http://example.com").unwrap();
let stream_body: crate::body::RequestBodySend = http_body_util::Empty::new()
.map_err(|never| match never {})
.boxed_unsync();
let rb = rb.body_stream(stream_body);
let cloned = rb.try_clone();
assert!(cloned.is_none());
}
#[tokio::test]
async fn upgrade_sets_headers() {
let client = test_client();
let rb = client.get("http://example.com").unwrap();
let rb = rb.upgrade();
let req = rb.build().unwrap();
assert_eq!(req.headers().get("connection").unwrap(), "Upgrade");
assert_eq!(req.headers().get("upgrade").unwrap(), "websocket");
assert_eq!(req.headers().get("sec-websocket-version").unwrap(), "13");
assert!(req.headers().get("sec-websocket-key").is_some());
assert_eq!(req.version(), Version::HTTP_11);
}
#[tokio::test]
async fn multipart_sets_content_type() {
let mp = crate::multipart::Multipart::new().text("field", "value");
let client = test_client();
let rb = client.post("http://example.com").unwrap();
let rb = rb.multipart(mp);
let req = rb.build().unwrap();
let ct = req.headers().get("content-type").unwrap().to_str().unwrap();
assert!(ct.starts_with("multipart/form-data; boundary="));
}
#[tokio::test]
async fn header_str_invalid_name() {
let client = test_client();
let rb = client.get("http://example.com").unwrap();
let result = rb.header_str("invalid header\n", "value");
assert!(result.is_err());
}
#[tokio::test]
async fn header_str_invalid_value() {
let client = test_client();
let rb = client.get("http://example.com").unwrap();
let result = rb.header_str("x-custom", "bad\0value");
assert!(result.is_err());
}
#[tokio::test]
async fn debug_request_builder() {
let client = test_client();
let rb = client.get("http://example.com/path").unwrap();
let dbg = format!("{rb:?}");
assert!(dbg.contains("RequestBuilderSend"));
assert!(dbg.contains("GET"));
assert!(dbg.contains("body: None"), "no body set, got: {dbg}");
}
#[tokio::test]
async fn debug_request_builder_with_body() {
let client = test_client();
let rb = client
.post("http://example.com/data")
.unwrap()
.body("payload");
let dbg = format!("{rb:?}");
assert!(dbg.contains("POST"));
assert!(
dbg.contains("Buffered"),
"body should show Buffered variant, got: {dbg}"
);
}
#[tokio::test]
async fn body_ref_returns_none_when_no_body_set() {
let client = test_client();
let rb = client.get("http://example.com/").unwrap();
assert!(rb.body_ref().is_none());
}
#[tokio::test]
async fn body_ref_returns_some_after_body_set() {
let client = test_client();
let rb = client.post("http://example.com/").unwrap().body("hello");
let body = rb.body_ref().expect("body should be set");
match body {
RequestBody::Buffered(b) => assert_eq!(&b[..], b"hello"),
#[cfg(not(target_arch = "wasm32"))]
_ => panic!("expected Buffered body"),
}
}
#[tokio::test]
async fn query_encodes_special_chars() {
let client = test_client();
let rb = client.get("http://example.com/path").unwrap();
let rb = rb.query(&[("key", "hello world"), ("tag", "a&b=c")]);
let req = rb.build().unwrap();
let uri = req.uri().to_string();
assert!(uri.contains("hello%20world"));
assert!(uri.contains("a%26b%3Dc"));
}
#[tokio::test]
async fn timeout_setter() {
let client = test_client();
let rb = client
.get("http://example.com")
.unwrap()
.timeout(Duration::from_secs(5));
let _req = rb.build().unwrap();
}
#[tokio::test]
async fn connect_timeout_setter() {
let client = test_client();
let rb = client
.get("http://example.com")
.unwrap()
.connect_timeout(Duration::from_secs(2));
assert_eq!(rb.connect_timeout, Some(Duration::from_secs(2)));
}
#[tokio::test]
async fn retry_setter() {
let client = test_client();
let rb = client
.get("http://example.com")
.unwrap()
.retry(RetryConfig::default());
let _req = rb.build().unwrap();
}
#[cfg(feature = "json")]
#[tokio::test]
async fn query_serde_empty_struct() {
#[derive(serde::Serialize)]
struct Empty {}
let client = test_client();
let rb = client.get("http://example.com/path").unwrap();
let rb = rb.query_serde(&Empty {}).unwrap();
let req = rb.build().unwrap();
let uri = req.uri().to_string();
assert!(!uri.contains('?'));
}
#[tokio::test]
async fn bearer_auth_with_invalid_chars_errors_on_build() {
let client = test_client();
let rb = client.get("http://example.com").unwrap();
let rb = rb.bearer_auth("token\x00with\x01control\x02chars");
let err = rb.build().unwrap_err();
assert!(matches!(err, Error::InvalidHeader(_)));
}
#[tokio::test]
async fn basic_auth_sets_header() {
let client = test_client();
let rb = client.get("http://example.com").unwrap();
let rb = rb.basic_auth("user", Some("pass"));
let req = rb.build().unwrap();
assert!(req.headers().contains_key("authorization"));
}
#[tokio::test]
async fn send_without_timeout_succeeds() {
let (addr, _counter) = aioduct_test_server::h1::h1_server().await;
let client = test_client();
let resp = client
.get(&format!("http://{addr}/"))
.unwrap()
.send()
.await
.unwrap();
assert_eq!(resp.status(), StatusCode::OK);
let body = resp.text().await.unwrap();
assert_eq!(body, "hello aioduct");
}
#[tokio::test]
async fn send_with_timeout_succeeds() {
let (addr, _counter) = aioduct_test_server::h1::h1_server().await;
let client = test_client();
let resp = client
.get(&format!("http://{addr}/"))
.unwrap()
.timeout(Duration::from_secs(5))
.send()
.await
.unwrap();
assert_eq!(resp.status(), StatusCode::OK);
}
#[tokio::test]
async fn retry_exhaustion_returns_error() {
let client = test_client();
let result = client
.get("http://127.0.0.1:1/")
.unwrap()
.timeout(Duration::from_millis(100))
.retry(
RetryConfig::default()
.max_retries(1)
.initial_backoff(Duration::from_millis(1))
.retry_on_status(false),
)
.send()
.await;
assert!(result.is_err(), "expected error from retry exhaustion");
}
#[tokio::test]
async fn retry_on_server_error_exhausts() {
let (addr, counter) = aioduct_test_server::h1::h1_server_with(|_req| async {
Ok(hyper::Response::builder()
.status(500)
.body(http_body_util::Full::new(bytes::Bytes::from("error")))
.unwrap())
})
.await;
let client = test_client();
let resp = client
.get(&format!("http://{addr}/"))
.unwrap()
.retry(
RetryConfig::default()
.max_retries(2)
.initial_backoff(Duration::from_millis(1))
.retry_on_status(true),
)
.send()
.await
.unwrap();
assert_eq!(resp.status(), StatusCode::INTERNAL_SERVER_ERROR);
assert_eq!(counter.requests(), 3);
}
#[tokio::test]
async fn retry_without_timeout_uses_no_timeout_path() {
use std::sync::Arc;
use std::sync::atomic::{AtomicU32, Ordering};
let attempt_count = Arc::new(AtomicU32::new(0));
let attempt_count2 = attempt_count.clone();
let (addr, _counter) = aioduct_test_server::h1::h1_server_with(move |_req| {
let count = attempt_count2.fetch_add(1, Ordering::Relaxed);
async move {
if count == 0 {
Ok(hyper::Response::builder()
.status(503)
.body(http_body_util::Full::new(bytes::Bytes::from("unavailable")))
.unwrap())
} else {
Ok(hyper::Response::new(http_body_util::Full::new(
bytes::Bytes::from("ok"),
)))
}
}
})
.await;
let client = test_client();
let resp = client
.get(&format!("http://{addr}/"))
.unwrap()
.retry(
RetryConfig::default()
.max_retries(2)
.initial_backoff(Duration::from_millis(1))
.retry_on_status(true),
)
.send()
.await
.unwrap();
assert_eq!(resp.status(), StatusCode::OK);
assert_eq!(attempt_count.load(Ordering::Relaxed), 2);
}
#[tokio::test]
async fn retry_after_is_used_for_retryable_request_timeout_status() {
use std::sync::Arc;
use std::sync::atomic::{AtomicU32, Ordering};
let attempt_count = Arc::new(AtomicU32::new(0));
let attempt_count2 = attempt_count.clone();
let (addr, _counter) = aioduct_test_server::h1::h1_server_with(move |_req| {
let count = attempt_count2.fetch_add(1, Ordering::Relaxed);
async move {
if count == 0 {
Ok(hyper::Response::builder()
.status(StatusCode::REQUEST_TIMEOUT)
.header(http::header::RETRY_AFTER, "0")
.body(http_body_util::Full::new(bytes::Bytes::from("timeout")))
.unwrap())
} else {
Ok(hyper::Response::new(http_body_util::Full::new(
bytes::Bytes::from("ok"),
)))
}
}
})
.await;
let client = test_client();
let send = client
.get(&format!("http://{addr}/"))
.unwrap()
.retry(
RetryConfig::default()
.max_retries(1)
.initial_backoff(Duration::from_secs(60))
.retry_on_status(true),
)
.send();
let resp = tokio::time::timeout(Duration::from_millis(500), send)
.await
.expect("Retry-After: 0 should override the configured backoff")
.unwrap();
assert_eq!(resp.status(), StatusCode::OK);
assert_eq!(attempt_count.load(Ordering::Relaxed), 2);
}
#[tokio::test]
async fn configured_retry_does_not_rerun_opaque_middleware() {
use std::sync::Arc;
use std::sync::atomic::{AtomicU32, Ordering};
let server_attempts = Arc::new(AtomicU32::new(0));
let attempts = server_attempts.clone();
let (addr, counter) = aioduct_test_server::h1::h1_server_with(move |request| {
let attempt = attempts.fetch_add(1, Ordering::SeqCst);
async move {
assert_eq!(request.method(), Method::PUT);
assert_eq!(request.headers()["x-finalized-request"], "first");
assert!(
request
.into_body()
.collect()
.await
.unwrap()
.to_bytes()
.is_empty()
);
Ok(if attempt == 0 {
hyper::Response::builder()
.status(StatusCode::SERVICE_UNAVAILABLE)
.body(http_body_util::Full::new(bytes::Bytes::new()))
.unwrap()
} else {
hyper::Response::new(http_body_util::Full::new(bytes::Bytes::from_static(b"ok")))
})
}
})
.await;
let middleware_calls = Arc::new(AtomicU32::new(0));
let calls = middleware_calls.clone();
let client = HttpEngineSend::<TokioRuntime, TcpConnector>::builder()
.middleware(
move |request: &mut http::Request<RequestBodySend>, _uri: &http::Uri| {
if calls.fetch_add(1, Ordering::SeqCst) == 0 {
*request.method_mut() = Method::PUT;
request.headers_mut().insert(
http::header::HeaderName::from_static("x-finalized-request"),
http::header::HeaderValue::from_static("first"),
);
} else {
*request.method_mut() = Method::POST;
*request.body_mut() =
http_body_util::Full::new(bytes::Bytes::from_static(b"different request"))
.map_err(|never| match never {})
.boxed_unsync();
}
},
)
.build()
.unwrap();
let response = client
.get(&format!("http://{addr}/"))
.unwrap()
.retry(
RetryConfig::default()
.max_retries(1)
.initial_backoff(Duration::ZERO),
)
.send()
.await
.unwrap();
assert_eq!(response.status(), StatusCode::OK);
assert_eq!(counter.requests(), 2);
assert_eq!(server_attempts.load(Ordering::SeqCst), 2);
assert_eq!(middleware_calls.load(Ordering::SeqCst), 1);
}
#[tokio::test]
async fn digest_retry_does_not_rerun_opaque_middleware() {
use std::sync::Arc;
use std::sync::atomic::{AtomicU32, Ordering};
let server_attempts = Arc::new(AtomicU32::new(0));
let attempts = server_attempts.clone();
let (addr, counter) = aioduct_test_server::h1::h1_server_with(move |request| {
let attempt = attempts.fetch_add(1, Ordering::SeqCst);
async move {
assert_eq!(request.method(), Method::POST);
assert_eq!(request.headers()["x-finalized-request"], "first");
if attempt > 0 {
assert!(request.headers().contains_key(http::header::AUTHORIZATION));
}
assert!(
request
.into_body()
.collect()
.await
.unwrap()
.to_bytes()
.is_empty()
);
Ok(if attempt == 0 {
hyper::Response::builder()
.status(StatusCode::UNAUTHORIZED)
.header(
http::header::WWW_AUTHENTICATE,
r#"Digest realm="replay", nonce="nonce", qop="auth""#,
)
.body(http_body_util::Full::new(bytes::Bytes::new()))
.unwrap()
} else {
hyper::Response::new(http_body_util::Full::new(bytes::Bytes::from_static(b"ok")))
})
}
})
.await;
let middleware_calls = Arc::new(AtomicU32::new(0));
let calls = middleware_calls.clone();
let client = HttpEngineSend::<TokioRuntime, TcpConnector>::builder()
.digest_auth("user", "pass")
.middleware(
move |request: &mut http::Request<RequestBodySend>, _uri: &http::Uri| {
if calls.fetch_add(1, Ordering::SeqCst) == 0 {
*request.method_mut() = Method::POST;
request.headers_mut().insert(
http::header::HeaderName::from_static("x-finalized-request"),
http::header::HeaderValue::from_static("first"),
);
} else {
*request.method_mut() = Method::DELETE;
*request.body_mut() =
http_body_util::Full::new(bytes::Bytes::from_static(b"different request"))
.map_err(|never| match never {})
.boxed_unsync();
}
},
)
.build()
.unwrap();
let response = client
.get(&format!("http://{addr}/"))
.unwrap()
.send()
.await
.unwrap();
assert_eq!(response.status(), StatusCode::OK);
assert_eq!(counter.requests(), 2);
assert_eq!(server_attempts.load(Ordering::SeqCst), 2);
assert_eq!(middleware_calls.load(Ordering::SeqCst), 1);
}
#[tokio::test]
async fn unsupported_digest_challenge_does_not_mutate_finalized_retry_state() {
use std::sync::atomic::{AtomicU32, Ordering};
use std::sync::{Arc, Mutex};
let (addr, counter) = aioduct_test_server::h1::h1_server_with(|_req| async {
Ok(hyper::Response::builder()
.status(StatusCode::UNAUTHORIZED)
.header(http::header::WWW_AUTHENTICATE, r#"Basic realm="fallback""#)
.body(http_body_util::Full::new(bytes::Bytes::new()))
.unwrap())
})
.await;
let middleware_calls = Arc::new(AtomicU32::new(0));
let calls = middleware_calls.clone();
let classified_method = Arc::new(Mutex::new(None));
let observed_method = classified_method.clone();
let client = HttpEngineSend::<TokioRuntime, TcpConnector>::builder()
.digest_auth("user", "pass")
.middleware(
move |request: &mut http::Request<RequestBodySend>, _uri: &http::Uri| {
if calls.fetch_add(1, Ordering::SeqCst) > 0 {
*request.method_mut() = Method::POST;
}
},
)
.build()
.unwrap();
let response = client
.get(&format!("http://{addr}/"))
.unwrap()
.retry(
RetryConfig::default()
.max_retries(1)
.initial_backoff(Duration::ZERO)
.classify(move |context| {
*observed_method.lock().unwrap() = Some(context.method().clone());
crate::retry::RetryDecision::DoNotRetry
}),
)
.send()
.await
.unwrap();
assert_eq!(response.status(), StatusCode::UNAUTHORIZED);
assert_eq!(counter.requests(), 1);
assert_eq!(middleware_calls.load(Ordering::SeqCst), 1);
assert_eq!(*classified_method.lock().unwrap(), Some(Method::GET));
}
#[tokio::test]
async fn digest_retry_respects_configured_retry_count() {
let (addr, counter) = aioduct_test_server::h1::h1_server_with(|_req| async {
Ok(hyper::Response::builder()
.status(StatusCode::UNAUTHORIZED)
.header(
http::header::WWW_AUTHENTICATE,
r#"Digest realm="count", nonce="nonce", qop="auth""#,
)
.body(http_body_util::Full::new(bytes::Bytes::new()))
.unwrap())
})
.await;
let client = HttpEngineSend::<TokioRuntime, TcpConnector>::builder()
.digest_auth("user", "pass")
.build()
.unwrap();
let response = client
.get(&format!("http://{addr}/"))
.unwrap()
.retry(RetryConfig::default().max_retries(0))
.send()
.await
.unwrap();
assert_eq!(response.status(), StatusCode::UNAUTHORIZED);
assert_eq!(counter.requests(), 1);
}
#[tokio::test]
async fn digest_retry_respects_configured_retry_budget() {
let (addr, counter) = aioduct_test_server::h1::h1_server_with(|_req| async {
Ok(hyper::Response::builder()
.status(StatusCode::UNAUTHORIZED)
.header(
http::header::WWW_AUTHENTICATE,
r#"Digest realm="budget", nonce="nonce", qop="auth""#,
)
.body(http_body_util::Full::new(bytes::Bytes::new()))
.unwrap())
})
.await;
let budget = crate::retry::RetryBudget::new(0, 0);
let client = HttpEngineSend::<TokioRuntime, TcpConnector>::builder()
.digest_auth("user", "pass")
.build()
.unwrap();
let response = client
.get(&format!("http://{addr}/"))
.unwrap()
.retry(RetryConfig::default().max_retries(3).budget(budget.clone()))
.send()
.await
.unwrap();
assert_eq!(response.status(), StatusCode::UNAUTHORIZED);
assert_eq!(counter.requests(), 1);
assert_eq!(budget.available(), 0);
}
#[tokio::test]
async fn send_error_contains_url() {
let client = test_client();
let err = client
.get("http://127.0.0.1:1/specific-path")
.unwrap()
.timeout(Duration::from_millis(100))
.send()
.await
.unwrap_err();
assert!(
err.url().to_string().contains("specific-path"),
"SendError should contain the request URL, got: {}",
err.url()
);
}
#[tokio::test]
async fn force_addr_setter() {
let client = test_client();
let addr: std::net::SocketAddr = "127.0.0.1:8080".parse().unwrap();
let rb = client.get("http://example.com").unwrap().force_addr(addr);
assert_eq!(rb.force_addr, Some(addr));
}
#[tokio::test]
async fn build_constructs_valid_request() {
let client = test_client();
let uri: Uri = "http://example.com/path?q=1".parse().unwrap();
let req = RequestBuilderSend::new(&client, Method::POST, uri, None)
.header(
HeaderName::from_static("x-custom"),
HeaderValue::from_static("value"),
)
.body("hello")
.build()
.unwrap();
assert_eq!(req.method(), Method::POST);
assert_eq!(req.uri().scheme_str(), Some("http"));
assert_eq!(req.uri().path(), "/path");
assert_eq!(req.uri().query(), Some("q=1"));
assert_eq!(req.headers().get("x-custom").unwrap(), "value");
}
#[tokio::test]
async fn header_overwrites_on_conflict() {
let client = test_client();
let rb = client.get("http://example.com").unwrap();
let rb = rb.header(
HeaderName::from_static("x-test"),
HeaderValue::from_static("first"),
);
let rb = rb.header(
HeaderName::from_static("x-test"),
HeaderValue::from_static("second"),
);
let req = rb.build().unwrap();
assert_eq!(
req.headers().get("x-test").unwrap(),
"second",
"second header() call should overwrite the first"
);
}
#[tokio::test]
async fn headers_keep_both_on_conflict() {
let client = test_client();
let mut hm = HeaderMap::new();
hm.insert(
HeaderName::from_static("x-test"),
HeaderValue::from_static("from_map"),
);
hm.insert(
HeaderName::from_static("x-other"),
HeaderValue::from_static("other"),
);
let rb = client.get("http://example.com").unwrap();
let rb = rb.headers(hm);
let rb = rb.header(
HeaderName::from_static("x-test"),
HeaderValue::from_static("override"),
);
let req = rb.build().unwrap();
assert_eq!(
req.headers().get("x-test").unwrap(),
"override",
"subsequent header() should overwrite the value from headers()"
);
assert_eq!(
req.headers().get("x-other").unwrap(),
"other",
"unrelated header from headers() should be preserved"
);
}
#[tokio::test]
async fn form_percent_encoding() {
let client = test_client();
let rb = client.post("http://example.com").unwrap();
let rb = rb.form(&[("key", "value with spaces"), ("special", "a&b=c%")]);
let req = rb.build().unwrap();
match req.into_body() {
RequestBody::Buffered(b) => {
let body_str = String::from_utf8(b.to_vec()).unwrap();
assert!(
body_str.contains("key=value+with+spaces"),
"spaces should become + in form encoding, got: {body_str}"
);
assert!(
body_str.contains("special=a%26b%3Dc%25"),
"special chars &, =, % should be percent-encoded, got: {body_str}"
);
}
_ => panic!("expected buffered body"),
}
}
#[tokio::test]
async fn custom_method_report() {
let client = test_client();
let method = Method::from_bytes(b"REPORT").unwrap();
let req = client
.request(method, "http://example.com")
.unwrap()
.build()
.unwrap();
assert_eq!(req.method().as_str(), "REPORT");
}
#[tokio::test]
async fn body_stream_sets_streaming_variant() {
let client = test_client();
let stream_body: crate::body::RequestBodySend = http_body_util::Empty::new()
.map_err(|never| match never {})
.boxed_unsync();
let rb = client
.post("http://example.com")
.unwrap()
.body_stream(stream_body);
assert!(
matches!(rb.body, Some(RequestBody::Streaming(_))),
"expected Streaming variant in builder's body field"
);
}
#[tokio::test]
async fn build_with_streaming_body() {
let client = test_client();
let stream_body: crate::body::RequestBodySend = http_body_util::Empty::new()
.map_err(|never| match never {})
.boxed_unsync();
let rb = client
.post("http://example.com")
.unwrap()
.body_stream(stream_body);
let req = rb.build().unwrap();
assert!(
matches!(req.into_body(), RequestBody::Streaming(_)),
"expected Streaming variant in built request body"
);
}
#[tokio::test]
async fn new_owned_creates_valid_request() {
let client = test_client();
let uri: Uri = "http://example.com/path".parse().unwrap();
let req = RequestBuilderSend::new_owned(client, Method::GET, uri, None)
.header_str("x-custom", "value")
.unwrap()
.build()
.unwrap();
assert_eq!(req.method(), Method::GET);
assert_eq!(req.uri().path(), "/path");
assert_eq!(req.headers().get("x-custom").unwrap(), "value");
}