use std::borrow::Cow;
use std::future::ready;
use std::time::Duration;
use bytesbuf::BytesView;
use futures::Stream;
use futures::future::Either;
use http::header::CONTENT_TYPE;
use http::{HeaderMap, HeaderName, HeaderValue, Method, Response, Version};
use templated_uri::Uri;
use crate::error_labels::LABEL_URI_MISSING;
use crate::http_utils::{CONTENT_TYPE_TEXT, try_content_length_header, try_header};
use crate::timeout::{BodyTimeout, ResponseTimeout};
use crate::{HttpBody, HttpBodyBuilder, HttpBodyOptions, HttpError, HttpRequest, HttpResponse, RequestHandler, Result};
#[derive(Debug)]
#[must_use]
pub struct HttpRequestBuilder<'a, R = ()> {
body_builder: Cow<'a, HttpBodyBuilder>,
builder: http::request::Builder,
uri: Option<Result<Uri>>,
body: Option<Result<HttpBody>>,
content_type: Option<HeaderValue>,
request_handler: &'a R,
}
impl HttpRequestBuilder<'static> {
#[cfg(any(feature = "test-util", test))]
pub fn new_fake() -> Self {
Self {
body_builder: Cow::Owned(HttpBodyBuilder::new_fake()),
builder: http::request::Builder::new(),
uri: None,
body: None,
content_type: None,
request_handler: &(),
}
}
}
impl<'a> HttpRequestBuilder<'a> {
pub fn new(builder: &'a HttpBodyBuilder) -> Self {
Self {
body_builder: Cow::Borrowed(builder),
builder: http::request::Builder::new(),
uri: None,
body: None,
content_type: None,
request_handler: &(),
}
}
}
impl<'a, R> HttpRequestBuilder<'a, R> {
pub fn with_request_handler(request_handler: &'a R, body_builder: &'a HttpBodyBuilder) -> Self {
Self {
builder: http::request::Builder::new(),
body_builder: Cow::Borrowed(body_builder),
uri: None,
body: None,
content_type: None,
request_handler,
}
}
}
impl<R> HttpRequestBuilder<'_, R> {
pub fn method(mut self, method: impl TryInto<http::Method, Error: Into<http::Error>>) -> Self {
self.builder = self.builder.method(method);
self
}
pub fn uri(mut self, uri: impl TryInto<Uri, Error: Into<HttpError>>) -> Self {
self.uri = Some(uri.try_into().map_err(Into::into));
self
}
pub fn get(self, uri: impl TryInto<Uri, Error: Into<HttpError>>) -> Self {
self.uri(uri).method(Method::GET)
}
pub fn post(self, uri: impl TryInto<Uri, Error: Into<HttpError>>) -> Self {
self.uri(uri).method(Method::POST)
}
pub fn delete(self, uri: impl TryInto<Uri, Error: Into<HttpError>>) -> Self {
self.uri(uri).method(Method::DELETE)
}
pub fn put(self, uri: impl TryInto<Uri, Error: Into<HttpError>>) -> Self {
self.uri(uri).method(Method::PUT)
}
pub fn patch(self, uri: impl TryInto<Uri, Error: Into<HttpError>>) -> Self {
self.uri(uri).method(Method::PATCH)
}
pub fn head(self, uri: impl TryInto<Uri, Error: Into<HttpError>>) -> Self {
self.uri(uri).method(Method::HEAD)
}
pub fn headers_mut(&mut self) -> Option<&mut HeaderMap<HeaderValue>> {
self.builder.headers_mut()
}
pub fn header(
mut self,
key: impl TryInto<HeaderName, Error: Into<http::Error>>,
value: impl TryInto<HeaderValue, Error: Into<http::Error>>,
) -> Self {
self.builder = self.builder.header(key, value);
self
}
pub fn version(mut self, version: Version) -> Self {
self.builder = self.builder.version(version);
self
}
pub fn extension<T>(mut self, extension: T) -> Self
where
T: Clone + Send + Sync + 'static,
{
self.builder = self.builder.extension(extension);
self
}
pub fn response_timeout(self, duration: Duration) -> Self {
self.extension(ResponseTimeout::new(duration))
}
pub fn body_timeout(self, duration: Duration) -> Self {
self.extension(BodyTimeout::new(duration))
}
pub fn timeout(self, duration: Duration) -> Self {
self.response_timeout(duration).body_timeout(duration)
}
pub fn text(mut self, data: impl AsRef<str>) -> Self {
let body = self.body_builder.text(data);
self.content_type = Some(CONTENT_TYPE_TEXT);
self.body(body)
}
pub fn bytes(self, b: impl Into<BytesView>) -> Self {
let body = self.body_builder.bytes(b);
self.body(body)
}
#[cfg(any(feature = "json", test))]
pub fn json<T: serde_core::ser::Serialize>(mut self, data: &T) -> Self {
let body = self.body_builder.json(data).map_err(HttpError::from);
self.content_type = Some(crate::http_utils::CONTENT_TYPE_JSON);
self.body_result(body)
}
pub fn body(self, body: HttpBody) -> Self {
self.body_result(Ok(body))
}
fn body_result(mut self, body: Result<HttpBody>) -> Self {
self.body = Some(body);
self
}
pub fn build(mut self) -> Result<HttpRequest> {
let body = self.body.take().unwrap_or_else(|| Ok(self.body_builder.empty()))?;
if let Some(length) = body.content_length() {
try_content_length_header(&mut self.builder, length);
}
if let Some(content_type) = self.content_type.take() {
try_header(&mut self.builder, CONTENT_TYPE, content_type);
}
let uri = self
.uri
.ok_or_else(|| HttpError::validation_with_label("URI is required when building the request", LABEL_URI_MISSING))??;
let path = uri.to_path_and_query();
let http_uri = http::Uri::try_from(uri.clone())?;
let mut request = self.builder.uri(http_uri).body(body)?;
request.extensions_mut().insert(crate::routing::RequestUris::new(uri));
if let Some(path) = path {
request.extensions_mut().insert(path);
}
Ok(request)
}
pub fn custom_body<B>(self, body: B) -> Self
where
B: http_body::Body<Data = BytesView, Error: Into<HttpError>> + Send + 'static,
{
let body = self.body_builder.body(body, &HttpBodyOptions::default());
self.body(body)
}
pub fn stream<S>(self, stream: S) -> Self
where
S: Stream<Item = Result<BytesView>> + Send + 'static,
{
let body = self.body_builder.stream(stream, &HttpBodyOptions::default());
self.body(body)
}
}
impl<R: RequestHandler> HttpRequestBuilder<'_, R> {
pub fn fetch(self) -> impl Future<Output = Result<HttpResponse>> + Send {
let handler = self.request_handler;
match self.build() {
Ok(request) => Either::Left(handler.execute(request)),
Err(err) => Either::Right(ready(Err(err))),
}
}
pub async fn fetch_buffered(self) -> Result<HttpResponse> {
let response = self.fetch().await?;
let (parts, body) = response.into_parts();
let body = body.into_buffered().await?;
Ok(HttpResponse::from_parts(parts, body))
}
pub async fn fetch_text(self) -> Result<Response<String>> {
let (parts, body) = self.fetch().await?.into_parts();
let body = body.into_text().await?;
Ok(Response::from_parts(parts, body))
}
pub async fn fetch_bytes(self) -> Result<Response<BytesView>> {
let (parts, body) = self.fetch().await?.into_parts();
let body = body.into_bytes().await?;
Ok(Response::from_parts(parts, body))
}
#[cfg(any(feature = "json", test))]
pub async fn fetch_json_owned<J: serde_core::de::DeserializeOwned>(self) -> Result<Response<J>> {
let (parts, body) = self.fetch().await?.into_parts();
let body = body.into_json_owned().await?;
Ok(Response::from_parts(parts, body))
}
#[cfg(any(feature = "json", test))]
pub async fn fetch_json<'de, J: serde_core::de::Deserialize<'de>>(self) -> Result<Response<crate::Json<J>>> {
let (parts, body) = self.fetch().await?.into_parts();
let body = body.into_json().await?;
Ok(Response::from_parts(parts, body))
}
}
#[cfg(test)]
#[cfg_attr(coverage_nightly, coverage(off))]
mod tests {
use futures::executor::block_on;
use http::StatusCode;
use http::header::CONTENT_LENGTH;
use ohno::{ErrorExt, Labeled};
use serde::{Deserialize, Serialize};
use super::*;
use crate::http_request_builder_ext::HttpRequestBuilderExt;
use crate::testing::{SingleChunkBody, create_stream_body_from_chunks};
use crate::{FakeHandler, HeaderMapExt, HttpResponseBuilder, RequestExt};
#[test]
fn new_with_borrowed_builder() {
let body_builder = HttpBodyBuilder::new_fake();
let request_builder = HttpRequestBuilder::new(&body_builder);
let request = request_builder
.method(Method::GET)
.uri("https://example.com")
.text("test")
.build()
.unwrap();
assert_eq!(block_on(request.into_body().into_text()).unwrap(), "test");
}
#[test]
fn json_body_ok() {
let request = HttpRequestBuilder::new_fake()
.method(Method::POST)
.uri("https://example.com")
.json(&JsonData { id: 42 })
.build()
.unwrap();
assert_eq!(request.headers().get_value_or(CONTENT_LENGTH, 0), 9);
assert_eq!(request.headers().get_str_value_or(CONTENT_TYPE, ""), "application/json");
assert_eq!(block_on(request.into_body().into_text()).unwrap(), "{\"id\":42}");
}
#[test]
fn json_does_not_override_existing_content_type() {
let request = HttpRequestBuilder::new_fake()
.method(Method::POST)
.uri("https://example.com")
.header(CONTENT_TYPE, "application/custom")
.json(&JsonData { id: 42 })
.build()
.unwrap();
assert_eq!(request.headers().get_str_value_or(CONTENT_TYPE, ""), "application/custom");
}
#[test]
fn text_body_ok() {
let request = HttpRequestBuilder::new_fake()
.method(Method::POST)
.uri("https://example.com")
.text("hello")
.build()
.unwrap();
assert_eq!(request.headers().get_value_or(CONTENT_LENGTH, 0), 5);
assert_eq!(request.headers().get_str_value_or(CONTENT_TYPE, ""), "text/plain");
assert_eq!(block_on(request.into_body().into_text()).unwrap(), "hello");
}
#[test]
fn text_does_not_override_existing_content_type() {
let request = HttpRequestBuilder::new_fake()
.method(Method::POST)
.uri("https://example.com")
.header(CONTENT_TYPE, "text/custom")
.text("hello")
.build()
.unwrap();
assert_eq!(request.headers().get_str_value_or(CONTENT_TYPE, ""), "text/custom");
}
#[test]
fn method_setting() {
let request = HttpRequestBuilder::new_fake()
.method(Method::PUT)
.uri("https://example.com")
.text("hello")
.build()
.unwrap();
assert_eq!(request.method(), Method::PUT);
}
#[test]
fn version_setting() {
let request = HttpRequestBuilder::new_fake()
.method(Method::GET)
.uri("https://example.com")
.version(Version::HTTP_2)
.text("hello")
.build()
.unwrap();
assert_eq!(request.version(), Version::HTTP_2);
}
#[test]
fn header_with_string_key_value() {
let request = HttpRequestBuilder::new_fake()
.method(Method::GET)
.uri("https://example.com")
.header("X-Custom-Header", "custom-value")
.text("hello")
.build()
.unwrap();
assert_eq!(request.headers().get("X-Custom-Header").unwrap(), "custom-value");
}
#[test]
fn header_with_header_name_value() {
let header_name = HeaderName::from_static("x-test-header");
let header_value = HeaderValue::from_static("test-value");
let request = HttpRequestBuilder::new_fake()
.method(Method::GET)
.uri("https://example.com")
.header(header_name.clone(), header_value.clone())
.text("hello")
.build()
.unwrap();
assert_eq!(request.headers().get(&header_name).unwrap(), &header_value);
}
#[test]
fn headers_mut_access() {
let mut request_builder = HttpRequestBuilder::new_fake();
if let Some(headers) = request_builder.headers_mut() {
headers.insert("X-Mut-Header", "mut-value".parse().unwrap());
}
let request = request_builder
.method(Method::GET)
.uri("https://example.com")
.text("hello")
.build()
.unwrap();
assert_eq!(request.headers().get("X-Mut-Header").unwrap(), "mut-value");
}
#[test]
fn multiple_headers() {
let request = HttpRequestBuilder::new_fake()
.method(Method::GET)
.uri("https://example.com")
.header("X-Header-1", "value1")
.header("X-Header-2", "value2")
.header("X-Header-3", "value3")
.text("hello")
.build()
.unwrap();
assert_eq!(request.headers().get("X-Header-1").unwrap(), "value1");
assert_eq!(request.headers().get("X-Header-2").unwrap(), "value2");
assert_eq!(request.headers().get("X-Header-3").unwrap(), "value3");
}
#[test]
fn direct_body_setting() {
let body = HttpBodyBuilder::new_fake().text("direct body");
let request = HttpRequestBuilder::new_fake()
.method(Method::POST)
.uri("https://example.com")
.body(body)
.build()
.unwrap();
assert_eq!(block_on(request.into_body().into_text()).unwrap(), "direct body");
}
#[test]
fn chained_operations() {
let request = HttpRequestBuilder::new_fake()
.method(Method::POST)
.uri("https://example.com")
.version(Version::HTTP_11)
.header("X-Custom", "value")
.header(CONTENT_TYPE, "application/custom")
.text("chained")
.build()
.unwrap();
assert_eq!(request.method(), Method::POST);
assert_eq!(request.version(), Version::HTTP_11);
assert_eq!(request.headers().get("X-Custom").unwrap(), "value");
assert_eq!(request.headers().get(CONTENT_TYPE).unwrap(), "application/custom");
assert_eq!(block_on(request.into_body().into_text()).unwrap(), "chained");
}
#[test]
fn custom_body_functionality() {
let builder = HttpBodyBuilder::new_fake();
let body = create_stream_body_from_chunks(&builder, &[b"custom", b" body", b" content"], &HttpBodyOptions::default());
let request = HttpRequestBuilder::new_fake()
.method(Method::POST)
.uri("https://example.com")
.body(body)
.build()
.unwrap();
assert_eq!(block_on(request.into_body().into_text()).unwrap(), "custom body content");
}
#[test]
fn custom_body_sets_body_from_custom_body_impl() {
let builder = HttpBodyBuilder::new_fake();
let request = HttpRequestBuilder::new_fake()
.post("https://example.com/upload")
.custom_body(SingleChunkBody::new(BytesView::copied_from_slice(b"external payload", &builder)))
.build()
.unwrap();
assert_eq!(block_on(request.into_body().into_text()).unwrap(), "external payload");
}
#[test]
fn stream_sets_body_from_chunks() {
let builder = HttpBodyBuilder::new_fake();
let chunks: Vec<crate::Result<BytesView>> = vec![
Ok(BytesView::copied_from_slice(b"hello ", &builder)),
Ok(BytesView::copied_from_slice(b"streaming ", &builder)),
Ok(BytesView::copied_from_slice(b"world", &builder)),
];
let request = HttpRequestBuilder::new_fake()
.post("https://example.com/upload")
.stream(futures::stream::iter(chunks))
.build()
.unwrap();
assert!(request.headers().get(CONTENT_LENGTH).is_none());
assert_eq!(block_on(request.into_body().into_text()).unwrap(), "hello streaming world");
}
#[test]
fn bytes_body_ok() {
let builder = HttpBodyBuilder::new_fake();
let request = HttpRequestBuilder::new_fake()
.method(Method::POST)
.uri("https://example.com")
.bytes(BytesView::copied_from_slice(b"hello", &builder))
.build()
.unwrap();
assert_eq!(request.headers().get_value_or(CONTENT_LENGTH, 0), 5);
assert!(request.headers().get(CONTENT_TYPE).is_none());
assert_eq!(block_on(request.into_body().into_bytes()).unwrap(), b"hello");
}
#[test]
fn empty_body_ok() {
let request = HttpRequestBuilder::new_fake()
.method(Method::GET)
.uri("https://example.com")
.build()
.unwrap();
assert_eq!(request.headers().get_value_or(CONTENT_LENGTH, -1), 0);
assert!(request.headers().get(CONTENT_TYPE).is_none());
assert_eq!(block_on(request.into_body().into_bytes()).unwrap().len(), 0,);
}
#[test]
fn uri_required() {
HttpRequestBuilder::new_fake()
.method(Method::GET)
.text("hello")
.build()
.unwrap_err();
}
#[derive(Serialize, Deserialize, Debug)]
struct JsonData {
id: u32,
}
#[derive(Deserialize, Debug, PartialEq)]
struct BorrowedJsonData<'a> {
id: u32,
#[serde(borrow)]
name: Cow<'a, str>,
#[serde(borrow)]
description: Cow<'a, str>,
}
#[test]
fn headers_mut_returns_none_on_error() {
let mut request_builder = HttpRequestBuilder::new_fake();
request_builder = request_builder.header("invalid\0header", "value");
assert!(request_builder.headers_mut().is_none());
}
#[test]
fn header_multiple_calls() {
let request = HttpRequestBuilder::new_fake()
.method(Method::POST)
.uri("https://example.com")
.header(CONTENT_TYPE, "application/custom1")
.header(CONTENT_TYPE, "application/custom2")
.build()
.unwrap();
let headers: Vec<_> = request.headers().get_all(CONTENT_TYPE).iter().collect();
assert_eq!(headers.len(), 2);
assert_eq!(headers[0].to_str().unwrap(), "application/custom1");
assert_eq!(headers[1].to_str().unwrap(), "application/custom2");
}
#[test]
fn content_type_preservation() {
let request = HttpRequestBuilder::new_fake()
.method(Method::POST)
.uri("https://example.com")
.json(&JsonData { id: 42 })
.build()
.unwrap();
assert_eq!(request.headers().get_value_or(CONTENT_LENGTH, 0), 9);
assert_eq!(request.headers().get_str_value_or(CONTENT_TYPE, ""), "application/json");
}
#[test]
fn request_build_error() {
let result = HttpRequestBuilder::new_fake()
.method(Method::GET)
.uri("https://example.com")
.header("invalid\0header", "value")
.build();
assert!(result.is_err());
let err = result.unwrap_err();
assert_eq!(err.message(), "invalid HTTP header name");
}
#[test]
fn fetch_json_borrowed_with_escaped_strings() {
let json_response = r#"{"id":123,"name":"John Doe","description":"A person with \"special\" characters: \n\t\\"}"#;
let client = FakeHandler::from_sync_handler(move |_request| {
let json_response = json_response.to_string();
HttpResponseBuilder::new_fake()
.status(StatusCode::OK)
.header(CONTENT_TYPE, "application/json")
.text(json_response)
.build()
});
let mut response = block_on(
client
.request_builder()
.uri("https://example.com/user")
.method(Method::GET)
.fetch_json::<BorrowedJsonData>(),
)
.unwrap()
.into_body();
let json_data = response.read().unwrap();
assert_eq!(json_data.id, 123);
assert_eq!(json_data.name, "John Doe");
let expected_description = "A person with \"special\" characters: \n\t\\";
assert_eq!(json_data.description, expected_description);
assert!(matches!(json_data.name, Cow::Borrowed(_)));
assert!(matches!(json_data.description, Cow::Owned(_)));
}
#[test]
fn json_deserialization_error() {
let client = FakeHandler::from_sync_handler(|_request| {
HttpResponseBuilder::new_fake()
.status(StatusCode::OK)
.text("corrupted json")
.build()
});
let result = block_on(
client
.request_builder()
.uri("https://example.com")
.method(Method::GET)
.fetch_json_owned::<JsonData>(),
);
assert!(result.is_err());
let err = result.unwrap_err();
assert!(err.message().contains("JSON deserialization error"));
}
#[test]
fn fetch_ok() {
let client =
FakeHandler::from_sync_handler(|_request| HttpResponseBuilder::new_fake().status(StatusCode::OK).text("response body").build());
let response = block_on(client.request_builder().uri("https://example.com").method(Method::GET).fetch()).unwrap();
assert_eq!(response.status(), StatusCode::OK);
assert_eq!(block_on(response.into_body().into_text()).unwrap(), "response body");
}
#[test]
fn fetch_buffered_ok() {
let client = FakeHandler::from_sync_handler(|_request| {
HttpResponseBuilder::new_fake()
.status(StatusCode::OK)
.text("buffered response")
.build()
});
let response = block_on(
client
.request_builder()
.uri("https://example.com")
.method(Method::GET)
.fetch_buffered(),
)
.unwrap();
assert_eq!(response.status(), StatusCode::OK);
assert_eq!(block_on(response.into_body().into_text()).unwrap(), "buffered response");
}
#[test]
fn fetch_text_ok() {
let client =
FakeHandler::from_sync_handler(|_request| HttpResponseBuilder::new_fake().status(StatusCode::OK).text("text response").build());
let response = block_on(client.request_builder().uri("https://example.com").method(Method::GET).fetch_text()).unwrap();
assert_eq!(response.status(), StatusCode::OK);
assert_eq!(response.into_body(), "text response");
}
#[test]
fn fetch_bytes_ok() {
let client = FakeHandler::from_sync_handler(|_request| {
HttpResponseBuilder::new_fake()
.status(StatusCode::OK)
.bytes(BytesView::copied_from_slice(b"BytesView response", &HttpBodyBuilder::new_fake()))
.build()
});
let response = block_on(
client
.request_builder()
.uri("https://example.com")
.method(Method::GET)
.fetch_bytes(),
)
.unwrap();
assert_eq!(response.status(), StatusCode::OK);
assert_eq!(response.into_body(), b"BytesView response");
}
#[test]
fn fetch_json_ok() {
let client = FakeHandler::from_sync_handler(|_request| {
HttpResponseBuilder::new_fake()
.status(StatusCode::OK)
.json(&JsonData { id: 42 })
.build()
});
let response = block_on(
client
.request_builder()
.uri("https://example.com")
.method(Method::GET)
.fetch_json::<JsonData>(),
)
.unwrap();
assert_eq!(response.status(), StatusCode::OK);
let json_data = response.into_body().read().unwrap();
assert_eq!(json_data.id, 42);
}
#[test]
fn fetch_json_owned_ok() {
let client = FakeHandler::from_sync_handler(|_request| {
HttpResponseBuilder::new_fake()
.status(StatusCode::OK)
.json(&JsonData { id: 123 })
.build()
});
let response = block_on(
client
.request_builder()
.uri("https://example.com")
.method(Method::GET)
.fetch_json_owned::<JsonData>(),
)
.unwrap();
assert_eq!(response.status(), StatusCode::OK);
assert_eq!(response.into_body().id, 123);
}
#[test]
fn fetch_with_request_validation() {
let client = FakeHandler::from_async_handler(|request| {
async move {
assert_eq!(request.method(), Method::POST);
assert_eq!(request.headers().get_str_value_or("x-test", ""), "chained");
assert_eq!(request.version(), http::Version::HTTP_2);
assert_eq!(request.into_body().into_text().await.unwrap(), "chained body");
HttpResponseBuilder::new_fake().status(StatusCode::CREATED).build()
}
});
let response = block_on(
client
.request_builder()
.uri("https://example.com")
.method(Method::POST)
.header("x-test", "chained")
.version(http::Version::HTTP_2)
.text("chained body")
.fetch(),
)
.unwrap();
assert_eq!(response.status(), StatusCode::CREATED);
}
#[test]
fn fetch_with_empty_body() {
let client = FakeHandler::from_async_handler(|request| async move {
assert_eq!(request.headers().get_value_or(CONTENT_LENGTH, -1), 0);
assert!(request.headers().get(CONTENT_TYPE).is_none());
let body_len = request.into_body().into_bytes().await.unwrap().len();
assert_eq!(body_len, 0);
HttpResponseBuilder::new_fake().status(StatusCode::OK).build()
});
block_on(client.request_builder().uri("https://example.com").method(Method::GET).fetch()).unwrap();
}
#[test]
fn fetch_with_json_body_validation() {
let client = FakeHandler::from_sync_handler(|request| {
assert_eq!(request.headers().get_value_or(CONTENT_LENGTH, 0), 9);
assert_eq!(request.headers().get_str_value_or(CONTENT_TYPE, ""), "application/json");
HttpResponseBuilder::new_fake().status(StatusCode::OK).build()
});
block_on(
client
.request_builder()
.uri("https://example.com")
.method(Method::POST)
.json(&JsonData { id: 42 })
.fetch(),
)
.unwrap();
}
#[test]
fn fetch_with_multiple_headers() {
let client = FakeHandler::from_sync_handler(|request| {
assert_eq!(request.headers().get_str_value_or("x-first", ""), "first");
assert_eq!(request.headers().get_str_value_or("x-second", ""), "second");
assert_eq!(request.version(), http::Version::HTTP_11);
HttpResponseBuilder::new_fake().status(StatusCode::OK).build()
});
block_on(
client
.request_builder()
.uri("https://example.com")
.method(Method::GET)
.header("x-first", "first")
.header("x-second", "second")
.version(http::Version::HTTP_11)
.fetch(),
)
.unwrap();
}
#[test]
fn get_method_sets_uri_and_method() {
let request = HttpRequestBuilder::new_fake().get("https://example.com/api").build().unwrap();
assert_eq!(request.method(), Method::GET);
assert_eq!(request.uri(), "https://example.com/api");
}
#[test]
fn post_method_sets_uri_and_method() {
let request = HttpRequestBuilder::new_fake()
.post("https://example.com/api")
.text("data")
.build()
.unwrap();
assert_eq!(request.method(), Method::POST);
assert_eq!(request.uri(), "https://example.com/api");
}
#[test]
fn delete_method_sets_uri_and_method() {
let request = HttpRequestBuilder::new_fake()
.delete("https://example.com/api/123")
.build()
.unwrap();
assert_eq!(request.method(), Method::DELETE);
assert_eq!(request.uri(), "https://example.com/api/123");
}
#[test]
fn put_method_sets_uri_and_method() {
let request = HttpRequestBuilder::new_fake()
.put("https://example.com/api/123")
.text("updated data")
.build()
.unwrap();
assert_eq!(request.method(), Method::PUT);
assert_eq!(request.uri(), "https://example.com/api/123");
}
#[test]
fn patch_method_sets_uri_and_method() {
let request = HttpRequestBuilder::new_fake()
.patch("https://example.com/api/123")
.text("partial update")
.build()
.unwrap();
assert_eq!(request.method(), Method::PATCH);
assert_eq!(request.uri(), "https://example.com/api/123");
}
#[test]
fn head_method_sets_uri_and_method() {
let request = HttpRequestBuilder::new_fake().head("https://example.com/api").build().unwrap();
assert_eq!(request.method(), Method::HEAD);
assert_eq!(request.uri(), "https://example.com/api");
assert_eq!(request.path_and_query().unwrap().to_string().declassify_ref(), "/api");
}
#[test]
fn method_convenience_functions_can_be_chained() {
let request = HttpRequestBuilder::new_fake()
.post("https://example.com/api")
.header("Authorization", "Bearer token")
.json(&JsonData { id: 42 })
.build()
.unwrap();
assert_eq!(request.method(), Method::POST);
assert_eq!(request.uri(), "https://example.com/api");
assert_eq!(request.headers().get_str_value_or("Authorization", ""), "Bearer token");
assert_eq!(block_on(request.into_body().into_text()).unwrap(), "{\"id\":42}");
}
#[test]
fn method_convenience_with_fetch() {
let client = FakeHandler::from_sync_handler(|request| {
assert_eq!(request.method(), Method::POST);
assert_eq!(request.uri(), "https://example.com/api");
HttpResponseBuilder::new_fake().status(StatusCode::CREATED).build()
});
let response = block_on(client.request_builder().post("https://example.com/api").text("test data").fetch()).unwrap();
assert_eq!(response.status(), StatusCode::CREATED);
}
#[test]
fn extension_attaches_to_request() {
use crate::UriTemplateLabel;
let request = HttpRequestBuilder::new_fake()
.get("https://example.com/api/users/123")
.extension(UriTemplateLabel::new("/api/users/{id}"))
.build()
.unwrap();
let label = request.extensions().get::<UriTemplateLabel>().expect("extension should be present");
assert_eq!(label.as_str(), "/api/users/{id}");
}
#[test]
fn build_attaches_request_uris_extension() {
use crate::routing::RequestUris;
let request = HttpRequestBuilder::new_fake()
.get("https://example.com/api/users/123")
.build()
.unwrap();
let uris = request
.extensions()
.get::<RequestUris>()
.expect("RequestUris extension should be present");
assert_eq!(uris.original().to_string().declassify_ref(), "https://example.com/api/users/123");
assert!(uris.routed().is_none(), "routed should be None before any router runs");
}
#[test]
fn extension_with_custom_type() {
#[derive(Clone, Debug, PartialEq)]
struct RequestId(String);
let request = HttpRequestBuilder::new_fake()
.get("https://example.com/api")
.extension(RequestId("req-123".to_string()))
.build()
.unwrap();
let id = request.extensions().get::<RequestId>().expect("extension should be present");
assert_eq!(id.0, "req-123");
}
#[test]
fn timeout_sets_both_response_and_body_timeout() {
use std::time::Duration;
use crate::timeout::{BodyTimeout, ResponseTimeout};
let request = HttpRequestBuilder::new_fake()
.get("https://example.com/api")
.timeout(Duration::from_secs(30))
.build()
.unwrap();
let response_timeout = request
.extensions()
.get::<ResponseTimeout>()
.expect("response timeout extension should be present");
assert_eq!(response_timeout.duration(), Duration::from_secs(30));
let body_timeout = request
.extensions()
.get::<BodyTimeout>()
.expect("body timeout extension should be present");
assert_eq!(body_timeout.duration(), Duration::from_secs(30));
}
#[test]
fn response_timeout_attaches_to_request() {
use std::time::Duration;
use crate::timeout::ResponseTimeout;
let request = HttpRequestBuilder::new_fake()
.get("https://example.com/api")
.response_timeout(Duration::from_secs(15))
.build()
.unwrap();
let timeout = request
.extensions()
.get::<ResponseTimeout>()
.expect("response timeout extension should be present");
assert_eq!(timeout.duration(), Duration::from_secs(15));
}
#[test]
fn body_timeout_attaches_to_request() {
use std::time::Duration;
use crate::timeout::BodyTimeout;
let request = HttpRequestBuilder::new_fake()
.get("https://example.com/api")
.body_timeout(Duration::from_mins(1))
.build()
.unwrap();
let timeout = request
.extensions()
.get::<BodyTimeout>()
.expect("body timeout extension should be present");
assert_eq!(timeout.duration(), Duration::from_mins(1));
}
#[test]
fn fetch_returns_error_when_build_fails() {
let handler = FakeHandler::from_sync_handler(|_request| {
HttpResponseBuilder::new_fake()
.status(StatusCode::OK)
.text("should not reach")
.build()
});
let result = block_on(handler.request_builder().method(Method::GET).fetch());
assert!(result.is_err());
let err = result.unwrap_err();
assert_eq!(err.label(), "uri_missing");
assert!(
err.message().contains("URI is required"),
"expected 'URI is required' but got: {}",
err.message()
);
}
}