Skip to content

Commit

Permalink
use hyperium/http instead of http-rs/http-types (#128)
Browse files Browse the repository at this point in the history
* use http instead of http-types

* chore: appease clippy::format_collect

* chore: appease some clippy::pedantic rules

* chore: remove unused `isahc`

* chore: use tokio::time::Sleep instead of futures_timer::Delay

* fix: HeaderExactMatcher is order sensitive

* chore(style): use `.len()` directly on the header value

* chore: appease clippy::write_with_newline

* chore: use Response::builder api

* chore: import header structs from http instead of hyper

* feat: update hyper v1.0

* chore: reqwest v0.11 still use old version `http`

* fix: use multi-threaded runtime to avoid blocking

* chore: switch back to single-thread runtime

* chore(docs): redundant explicit link target

* fixup! chore: import header structs from http instead of hyper

---------

Co-authored-by: Luca Palmieri <[email protected]>
  • Loading branch information
flisky and LukeMathWalker authored Nov 30, 2023
1 parent d61239e commit 8047af3
Show file tree
Hide file tree
Showing 13 changed files with 205 additions and 292 deletions.
9 changes: 5 additions & 4 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -20,24 +20,25 @@ path = "src/lib.rs"

[dependencies]
log = "0.4"
http-types = { version = "2.11", default-features = false, features = ["hyperium_http"] }
serde_json = "1"
serde = "1"
regex = "1"
futures-timer = "3.0.2"
futures = "0.3.5"
hyper = { version = "0.14", features = ["full"] }
http = "1.0"
http-body-util = "0.1"
hyper = { version = "1.0", features = ["full"] }
hyper-util = { version = "0.1", features = ["tokio"] }
tokio = { version = "1.5.0", features = ["rt"] }
deadpool = "0.9.2"
async-trait = "0.1"
once_cell = "1"
assert-json-diff = "2.0.1"
base64 = "0.21.0"
url = "2.2"

[dev-dependencies]
async-std = { version = "1.9.0", features = ["attributes"] }
surf = "2.3.2"
reqwest = "0.11.3"
tokio = { version = "1.5.0", features = ["macros", "rt-multi-thread"] }
actix-rt = "2.2.0"
isahc = "1.3.1"
6 changes: 3 additions & 3 deletions src/http.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
//! Convenient re-exports of `http-types`' types that are part of `wiremock`'s public API.
pub use http_types::headers::{HeaderName, HeaderValue, HeaderValues};
pub use http_types::{Method, Url};
//! Convenient re-exports of http types that are part of `wiremock`'s public API.
pub use http::{HeaderMap, HeaderName, HeaderValue, Method};
pub use url::Url;
76 changes: 41 additions & 35 deletions src/matchers.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,15 +10,14 @@
use crate::{Match, Request};
use assert_json_diff::{assert_json_matches_no_panic, CompareMode};
use base64::prelude::{Engine as _, BASE64_STANDARD};
use http_types::headers::{HeaderName, HeaderValue, HeaderValues};
use http_types::{Method, Url};
use http::{HeaderName, HeaderValue, Method};
use log::debug;
use regex::Regex;
use serde::Serialize;
use serde_json::Value;
use std::convert::TryInto;
use std::ops::Deref;
use std::str;
use url::Url;

/// Implement the `Match` trait for all closures, out of the box,
/// if their signature is compatible.
Expand Down Expand Up @@ -342,7 +341,7 @@ impl Match for PathRegexMatcher {
/// assert_eq!(status, 200);
/// }
/// ```
pub struct HeaderExactMatcher(HeaderName, HeaderValues);
pub struct HeaderExactMatcher(HeaderName, Vec<HeaderValue>);

/// Shorthand for [`HeaderExactMatcher::new`].
pub fn header<K, V>(key: K, value: V) -> HeaderExactMatcher
Expand All @@ -352,7 +351,7 @@ where
V: TryInto<HeaderValue>,
<V as TryInto<HeaderValue>>::Error: std::fmt::Debug,
{
HeaderExactMatcher::new(key, value.try_into().map(HeaderValues::from).unwrap())
HeaderExactMatcher::new(key, vec![value])
}

/// Shorthand for [`HeaderExactMatcher::new`] supporting multi valued headers.
Expand All @@ -363,38 +362,44 @@ where
V: TryInto<HeaderValue>,
<V as TryInto<HeaderValue>>::Error: std::fmt::Debug,
{
let values = values
.into_iter()
.filter_map(|v| v.try_into().ok())
.collect::<HeaderValues>();
HeaderExactMatcher::new(key, values)
}

impl HeaderExactMatcher {
pub fn new<K, V>(key: K, value: V) -> Self
pub fn new<K, V>(key: K, values: Vec<V>) -> Self
where
K: TryInto<HeaderName>,
<K as TryInto<HeaderName>>::Error: std::fmt::Debug,
V: TryInto<HeaderValues>,
<V as TryInto<HeaderValues>>::Error: std::fmt::Debug,
V: TryInto<HeaderValue>,
<V as TryInto<HeaderValue>>::Error: std::fmt::Debug,
{
let key = key.try_into().expect("Failed to convert to header name.");
let value = value
.try_into()
.expect("Failed to convert to header value.");
Self(key, value)
let values = values
.into_iter()
.map(|value| {
value
.try_into()
.expect("Failed to convert to header value.")
})
.collect();
Self(key, values)
}
}

impl Match for HeaderExactMatcher {
fn matches(&self, request: &Request) -> bool {
match request.headers.get(&self.0) {
None => false,
Some(values) => {
let headers: Vec<&str> = self.1.iter().map(HeaderValue::as_str).collect();
values.eq(headers.as_slice())
}
}
let values = request
.headers
.get_all(&self.0)
.iter()
.filter_map(|v| v.to_str().ok())
.flat_map(|v| {
v.split(',')
.map(str::trim)
.filter_map(|v| HeaderValue::from_str(v).ok())
})
.collect::<Vec<_>>();
values == self.1 // order matters
}
}

Expand Down Expand Up @@ -513,12 +518,16 @@ impl HeaderRegexMatcher {

impl Match for HeaderRegexMatcher {
fn matches(&self, request: &Request) -> bool {
match request.headers.get(&self.0) {
None => false,
Some(values) => {
let has_values = values.iter().next().is_some();
has_values && values.iter().all(|v| self.1.is_match(v.as_str()))
}
let mut it = request
.headers
.get_all(&self.0)
.iter()
.filter_map(|v| v.to_str().ok())
.peekable();
if it.peek().is_some() {
it.all(|v| self.1.is_match(v))
} else {
false
}
}
}
Expand Down Expand Up @@ -994,7 +1003,6 @@ where
/// use wiremock::{MockServer, Mock, ResponseTemplate};
/// use wiremock::matchers::basic_auth;
/// use serde::{Deserialize, Serialize};
/// use http_types::auth::BasicAuth;
/// use std::convert::TryInto;
///
/// #[async_std::main]
Expand All @@ -1008,10 +1016,9 @@ where
/// .mount(&mock_server)
/// .await;
///
/// let auth = BasicAuth::new("username", "password");
/// let client: surf::Client = surf::Config::new()
/// .set_base_url(surf::Url::parse(&mock_server.uri()).unwrap())
/// .add_header(auth.name(), auth.value()).unwrap()
/// .add_header("Authorization", "Basic dXNlcm5hbWU6cGFzc3dvcmQ=").unwrap()
/// .try_into().unwrap();
///
/// // Act
Expand Down Expand Up @@ -1040,7 +1047,7 @@ impl BasicAuthMatcher {
pub fn from_token(token: impl AsRef<str>) -> Self {
Self(header(
"Authorization",
format!("Basic {}", token.as_ref()).deref(),
&*format!("Basic {}", token.as_ref()),
))
}
}
Expand Down Expand Up @@ -1069,7 +1076,6 @@ impl Match for BasicAuthMatcher {
/// use wiremock::{MockServer, Mock, ResponseTemplate};
/// use wiremock::matchers::bearer_token;
/// use serde::{Deserialize, Serialize};
/// use http_types::auth::BasicAuth;
///
/// #[async_std::main]
/// async fn main() {
Expand Down Expand Up @@ -1098,7 +1104,7 @@ impl BearerTokenMatcher {
pub fn from_token(token: impl AsRef<str>) -> Self {
Self(header(
"Authorization",
format!("Bearer {}", token.as_ref()).deref(),
&*format!("Bearer {}", token.as_ref()),
))
}
}
Expand Down
8 changes: 4 additions & 4 deletions src/mock.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,13 +15,13 @@ use std::ops::{
/// use std::convert::TryInto;
///
/// // Check that a header with the specified name exists and its value has an odd length.
/// pub struct OddHeaderMatcher(http_types::headers::HeaderName);
/// pub struct OddHeaderMatcher(http::HeaderName);
///
/// impl Match for OddHeaderMatcher {
/// fn matches(&self, request: &Request) -> bool {
/// match request.headers.get(&self.0) {
/// // We are ignoring multi-valued headers for simplicity
/// Some(values) => values[0].as_str().len() % 2 == 1,
/// Some(value) => value.to_str().unwrap_or_default().len() % 2 == 1,
/// None => false
/// }
/// }
Expand Down Expand Up @@ -69,11 +69,11 @@ use std::ops::{
/// // Arrange
/// let mock_server = MockServer::start().await;
///
/// let header_name: http_types::headers::HeaderName = "custom".try_into().unwrap();
/// let header_name = http::HeaderName::from_static("custom");
/// // Check that a header with the specified name exists and its value has an odd length.
/// let matcher = move |request: &Request| {
/// match request.headers.get(&header_name) {
/// Some(values) => values[0].as_str().len() % 2 == 1,
/// Some(value) => value.to_str().unwrap_or_default().len() % 2 == 1,
/// None => false
/// }
/// };
Expand Down
33 changes: 16 additions & 17 deletions src/mock_server/bare_server.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,15 @@ use crate::mock_set::MockId;
use crate::mock_set::MountedMockSet;
use crate::request::BodyPrintLimit;
use crate::{mock::Mock, verification::VerificationOutcome, Request};
use http_body_util::Full;
use hyper::body::Bytes;
use std::fmt::{Debug, Write};
use std::net::{SocketAddr, TcpListener, TcpStream};
use std::pin::pin;
use std::sync::atomic::AtomicBool;
use std::sync::Arc;
use tokio::sync::Notify;
use tokio::sync::RwLock;
use tokio::task::LocalSet;

/// An HTTP web-server running in the background to behave as one of your dependencies using `Mock`s
/// for testing purposes.
Expand All @@ -22,7 +23,7 @@ pub(crate) struct BareMockServer {
state: Arc<RwLock<MockServerState>>,
server_address: SocketAddr,
// When `_shutdown_trigger` gets dropped the listening server terminates gracefully.
_shutdown_trigger: tokio::sync::oneshot::Sender<()>,
_shutdown_trigger: tokio::sync::watch::Sender<()>,
}

/// The elements of [`BareMockServer`] that are affected by each incoming request.
Expand All @@ -38,7 +39,7 @@ impl MockServerState {
pub(super) async fn handle_request(
&mut self,
mut request: Request,
) -> (http_types::Response, Option<futures_timer::Delay>) {
) -> (hyper::Response<Full<Bytes>>, Option<tokio::time::Sleep>) {
request.body_print_limit = self.body_print_limit;
// If request recording is enabled, record the incoming request
// by adding it to the `received_requests` stack
Expand All @@ -57,7 +58,7 @@ impl BareMockServer {
request_recording: RequestRecording,
body_print_limit: BodyPrintLimit,
) -> Self {
let (shutdown_trigger, shutdown_receiver) = tokio::sync::oneshot::channel();
let (shutdown_trigger, shutdown_receiver) = tokio::sync::watch::channel(());
let received_requests = match request_recording {
RequestRecording::Enabled => Some(Vec::new()),
RequestRecording::Disabled => None,
Expand All @@ -80,15 +81,15 @@ impl BareMockServer {
.build()
.expect("Cannot build local tokio runtime");

LocalSet::new().block_on(&runtime, server_future)
runtime.block_on(server_future);
});
for _ in 0..40 {
if TcpStream::connect_timeout(&server_address, std::time::Duration::from_millis(25))
.is_ok()
{
break;
}
futures_timer::Delay::new(std::time::Duration::from_millis(25)).await;
tokio::time::sleep(std::time::Duration::from_millis(25)).await;
}

Self {
Expand Down Expand Up @@ -162,7 +163,7 @@ impl BareMockServer {
/// If request recording was disabled, it returns `None`.
pub(crate) async fn received_requests(&self) -> Option<Vec<Request>> {
let state = self.state.read().await;
state.received_requests.to_owned()
state.received_requests.clone()
}
}

Expand Down Expand Up @@ -270,7 +271,7 @@ impl MockGuard {
}

// await event
notification.await
notification.await;
}
}

Expand All @@ -292,15 +293,13 @@ impl Drop for MockGuard {
if received_requests.is_empty() {
"The server did not receive any request.".into()
} else {
let requests = received_requests.iter().enumerate().fold(
String::new(),
|mut r, (index, request)| {
write!(r, "- Request #{idx}\n\t{request}", idx = index + 1,)
.unwrap();
r
received_requests.iter().enumerate().fold(
"Received requests:\n".to_string(),
|mut message, (index, request)| {
_ = write!(message, "- Request #{}\n\t{}", index + 1, request);
message
},
);
format!("Received requests:\n{requests}")
)
}
} else {
"Enable request recording on the mock server to get the list of incoming requests as part of the panic message.".into()
Expand All @@ -320,6 +319,6 @@ impl Drop for MockGuard {
state.mock_set.deactivate(*mock_id);
}
};
futures::executor::block_on(future)
futures::executor::block_on(future);
}
}
Loading

0 comments on commit 8047af3

Please sign in to comment.