use std::time::Duration;
use bytes::Bytes;
use fetch::options::{ConnectionLifetime, ConnectionPoolOptions, PoolIndex};
use fetch::telemetry::ConnectionInfo;
use fetch::tokio::TokioDeps;
use fetch::{HttpClient, HttpClientBuilder};
use http::{StatusCode, Version};
use http_extensions::HttpBodyOptions;
use ohno::{ErrorExt, assert_error_message};
use tick::ClockControl;
use wiremock::matchers::{method, path};
use wiremock::{Mock, MockServer, ResponseTemplate};
#[cfg_attr(miri, ignore)]
#[tokio::test]
async fn response_ok() {
let client = create_builder().build();
let server = serve(Bytes::from("Hello World!")).await;
let response = client.get(server.uri() + "/hello-world").fetch_text().await.unwrap();
assert_eq!(response.status(), StatusCode::OK);
}
#[cfg_attr(miri, ignore)]
#[tokio::test]
async fn response_buffering_limit() {
let client = create_builder()
.response_body_options(HttpBodyOptions::default().buffer_limit(1024))
.build();
let content = vec![0; 1025];
let server = serve(Bytes::from(content)).await;
let error = client.get(server.uri() + "/hello-world").fetch_buffered().await.unwrap_err();
assert_eq!(error.message(), "body size exceeds the limit of 1024 bytes");
}
#[cfg_attr(miri, ignore)]
#[tokio::test]
async fn no_tls_http_1_selected() {
let client = create_builder().build();
let server = serve(Bytes::from_static(b"hello")).await;
let version = client.get(server.uri() + "/hello-world").fetch().await.unwrap().version();
assert_eq!(version, Version::HTTP_11);
}
#[cfg_attr(miri, ignore)]
#[tokio::test]
async fn no_tls_http_2_accepted() {
let client = create_builder().supported_http_versions(&[Version::HTTP_2]).build();
let server = serve(Bytes::from_static(b"hello")).await;
let _response = client.get(server.uri() + "/hello-world").fetch().await.unwrap();
}
#[cfg_attr(miri, ignore)]
#[tokio::test]
async fn no_tls_http_2_rejected() {
let client = create_builder()
.supported_http_versions(&[Version::HTTP_2, Version::HTTP_3])
.build();
let server = serve(Bytes::from_static(b"hello")).await;
let error = client.get(server.uri() + "/hello-world").fetch().await.unwrap_err();
assert_error_message!(error, "client error (Connect)");
error.find_source_with::<fetch::HttpError>(|e| {
e.message()
== "the connection was established with unsupported HTTP version: HTTP/1.1, supported versions are: [HTTP/2.0, HTTP/3.0]"
});
}
#[cfg_attr(miri, ignore)]
#[tokio::test]
async fn no_tls_http_3_rejected() {
let client = create_builder()
.supported_http_versions(&[Version::HTTP_2, Version::HTTP_3])
.build();
let server = serve(Bytes::from_static(b"hello")).await;
let error = client.get(server.uri() + "/hello-world").fetch().await.unwrap_err();
assert_error_message!(error, "client error (Connect)");
error.find_source_with::<fetch::HttpError>(|e| {
e.message()
== "the connection was established with unsupported HTTP version: HTTP/1.1, supported versions are: [HTTP/2.0, HTTP/3.0]"
});
}
#[cfg_attr(miri, ignore)]
#[tokio::test]
#[cfg(feature = "json")]
async fn json_owned() {
use serde::Deserialize;
#[derive(Deserialize, Debug)]
struct Person {
name: String,
surname: String,
}
let json = Bytes::from_static(br#"{"name": "John", "surname": "Doe"}"#);
let client = create_builder().build();
let server = serve(json).await;
let person = client
.get(server.uri() + "/hello-world")
.fetch_json_owned::<Person>()
.await
.unwrap()
.into_body();
assert_eq!(person.name, "John");
assert_eq!(person.surname, "Doe");
}
#[cfg_attr(miri, ignore)]
#[tokio::test]
#[cfg(feature = "json")]
async fn json_borrowed() {
use serde::Deserialize;
#[derive(Deserialize, Debug)]
struct Person<'a> {
name: std::borrow::Cow<'a, str>,
surname: std::borrow::Cow<'a, str>,
}
let json = Bytes::from_static(br#"{"name": "John", "surname": "Doe"}"#);
let client = create_builder().build();
let server = serve(json).await;
let mut json = client
.get(server.uri() + "/hello-world")
.fetch_json::<Person>()
.await
.unwrap()
.into_body();
let person = json.read().unwrap();
assert_eq!(person.name, "John");
assert_eq!(person.surname, "Doe");
}
#[cfg_attr(miri, ignore)]
#[tokio::test]
async fn connection_info_attached_and_recreated_after_max_lifetime() {
let control = ClockControl::new();
let max_age = Duration::from_mins(1);
let client = HttpClient::builder_tokio(TokioDeps::with_clock(&control.to_clock()))
.insecure_allow_http()
.connection_pool_options(ConnectionPoolOptions::default().connection_lifetime(ConnectionLifetime::fixed(max_age)))
.build();
let server = serve(Bytes::from_static(b"hello")).await;
let url = server.uri() + "/hello-world";
let response = client.get(url.clone()).fetch().await.unwrap();
let info = response
.extensions()
.get::<ConnectionInfo>()
.expect("ConnectionInfo must be attached to every response served by a real connection");
assert_eq!(info.pool_index(), PoolIndex::new(0));
assert!(!info.is_poisoned());
control.advance(max_age + Duration::from_secs(1));
let response = client.get(url.clone()).fetch().await.unwrap();
let expired_info = response
.extensions()
.get::<ConnectionInfo>()
.expect("ConnectionInfo must be attached to every response served by a real connection");
assert!(
expired_info.is_poisoned(),
"expected the connection to be poisoned once its age exceeded max_age",
);
let response = client.get(url).fetch().await.unwrap();
let fresh_info = response
.extensions()
.get::<ConnectionInfo>()
.expect("ConnectionInfo must be attached to every response served by a real connection");
assert!(!fresh_info.is_poisoned(), "a newly established connection must not be poisoned");
}
fn create_builder() -> HttpClientBuilder {
HttpClient::builder_tokio(TokioDeps::default()).insecure_allow_http()
}
async fn serve(body: impl Into<Bytes>) -> MockServer {
let mock_server = MockServer::start().await;
Mock::given(method("GET"))
.and(path("/hello-world"))
.respond_with(ResponseTemplate::new(200).set_body_bytes(body.into().to_vec()))
.mount(&mock_server)
.await;
mock_server
}