Skip to content

Commit ba2e0c7

Browse files
authored
Allow returning arbitrary errors (#159)
* Allow returning arbitrary errors Add `MockBuilder::respond_with_err` to respond with an arbitrary Rust error instead of an HTTP error. Due to overlapping impl constraints, `RespondErr` only supports passing a function that returns an error and not the error itself. Fixes #149 * Add tests * Skip unstable error message
1 parent d007b1f commit ba2e0c7

File tree

8 files changed

+146
-26
lines changed

8 files changed

+146
-26
lines changed

src/lib.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -155,6 +155,8 @@ mod respond;
155155
mod response_template;
156156
mod verification;
157157

158+
pub type ErrorResponse = Box<dyn std::error::Error + Send + Sync + 'static>;
159+
158160
pub use mock::{Match, Mock, MockBuilder, Times};
159161
pub use mock_server::{MockGuard, MockServer, MockServerBuilder};
160162
pub use request::Request;

src/mock.rs

Lines changed: 28 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
1-
use crate::respond::Respond;
2-
use crate::{MockGuard, MockServer, Request, ResponseTemplate};
1+
use crate::respond::{Respond, RespondErr};
2+
use crate::{ErrorResponse, MockGuard, MockServer, Request, ResponseTemplate};
33
use std::fmt::{Debug, Formatter};
44
use std::ops::{
55
Range, RangeBounds, RangeFrom, RangeFull, RangeInclusive, RangeTo, RangeToInclusive,
@@ -270,7 +270,7 @@ impl Debug for Matcher {
270270
#[must_use = "`Mock`s have to be mounted or registered with a `MockServer` to become effective"]
271271
pub struct Mock {
272272
pub(crate) matchers: Vec<Matcher>,
273-
pub(crate) response: Box<dyn Respond>,
273+
pub(crate) response: Result<Box<dyn Respond>, Box<dyn RespondErr>>,
274274
/// Maximum number of times (inclusive) we should return a response from this Mock on
275275
/// matching requests.
276276
/// If `None`, there is no cap and we will respond to all incoming matching requests.
@@ -643,8 +643,14 @@ impl Mock {
643643

644644
/// Given a [`Request`] build an instance a [`ResponseTemplate`] using
645645
/// the responder associated with the `Mock`.
646-
pub(crate) fn response_template(&self, request: &Request) -> ResponseTemplate {
647-
self.response.respond(request)
646+
pub(crate) fn response_template(
647+
&self,
648+
request: &Request,
649+
) -> Result<ResponseTemplate, ErrorResponse> {
650+
match &self.response {
651+
Ok(responder) => Ok(responder.respond(request)),
652+
Err(responder_err) => Err(responder_err.respond_err(request)),
653+
}
648654
}
649655
}
650656

@@ -670,7 +676,23 @@ impl MockBuilder {
670676
pub fn respond_with<R: Respond + 'static>(self, responder: R) -> Mock {
671677
Mock {
672678
matchers: self.matchers,
673-
response: Box::new(responder),
679+
response: Ok(Box::new(responder)),
680+
max_n_matches: None,
681+
priority: 5,
682+
name: None,
683+
expectation_range: Times(TimesEnum::Unbounded(RangeFull)),
684+
}
685+
}
686+
687+
/// Instead of response with an HTTP reply, return a Rust error.
688+
///
689+
/// This can simulate lower level errors, e.g., a [`ConnectionReset`] IO Error.
690+
///
691+
/// [`ConnectionReset`]: std::io::ErrorKind::ConnectionReset
692+
pub fn respond_with_err<R: RespondErr + 'static>(self, responder_err: R) -> Mock {
693+
Mock {
694+
matchers: self.matchers,
695+
response: Err(Box::new(responder_err)),
674696
max_n_matches: None,
675697
priority: 5,
676698
name: None,

src/mock_server/bare_server.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ use crate::mock_server::hyper::run_server;
22
use crate::mock_set::MockId;
33
use crate::mock_set::MountedMockSet;
44
use crate::request::BodyPrintLimit;
5-
use crate::{mock::Mock, verification::VerificationOutcome, Request};
5+
use crate::{mock::Mock, verification::VerificationOutcome, ErrorResponse, Request};
66
use http_body_util::Full;
77
use hyper::body::Bytes;
88
use std::fmt::{Debug, Write};
@@ -39,7 +39,7 @@ impl MockServerState {
3939
pub(super) async fn handle_request(
4040
&mut self,
4141
request: Request,
42-
) -> (hyper::Response<Full<Bytes>>, Option<tokio::time::Sleep>) {
42+
) -> Result<(hyper::Response<Full<Bytes>>, Option<tokio::time::Sleep>), ErrorResponse> {
4343
// If request recording is enabled, record the incoming request
4444
// by adding it to the `received_requests` stack
4545
if let Some(received_requests) = &mut self.received_requests {

src/mock_server/hyper.rs

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,17 @@ use std::sync::Arc;
55
use tokio::net::TcpListener;
66
use tokio::sync::RwLock;
77

8+
/// Work around a lifetime error where, for some reason,
9+
/// `Box<dyn std::error::Error + Send + Sync + 'static>` can't be converted to a
10+
/// `Box<dyn std::error::Error + Send + Sync>`
11+
struct ErrorLifetimeCast(Box<dyn std::error::Error + Send + Sync + 'static>);
12+
13+
impl From<ErrorLifetimeCast> for Box<dyn std::error::Error + Send + Sync> {
14+
fn from(value: ErrorLifetimeCast) -> Self {
15+
value.0
16+
}
17+
}
18+
819
/// The actual HTTP server responding to incoming requests according to the specified mocks.
920
pub(super) async fn run_server(
1021
listener: std::net::TcpListener,
@@ -24,7 +35,8 @@ pub(super) async fn run_server(
2435
.write()
2536
.await
2637
.handle_request(wiremock_request)
27-
.await;
38+
.await
39+
.map_err(ErrorLifetimeCast)?;
2840

2941
// We do not wait for the delay within the handler otherwise we would be
3042
// holding on to the write-side of the `RwLock` on `mock_set`.
@@ -38,7 +50,7 @@ pub(super) async fn run_server(
3850
delay.await;
3951
}
4052

41-
Ok::<_, &'static str>(response)
53+
Ok::<_, ErrorLifetimeCast>(response)
4254
}
4355
};
4456

src/mock_set.rs

Lines changed: 16 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,9 @@ use crate::request::BodyPrintLimit;
22
use crate::{
33
mounted_mock::MountedMock,
44
verification::{VerificationOutcome, VerificationReport},
5+
ErrorResponse,
56
};
6-
use crate::{Mock, Request, ResponseTemplate};
7+
use crate::{Mock, Request};
78
use http_body_util::Full;
89
use hyper::body::Bytes;
910
use log::debug;
@@ -56,9 +57,9 @@ impl MountedMockSet {
5657
pub(crate) async fn handle_request(
5758
&mut self,
5859
request: Request,
59-
) -> (hyper::Response<Full<Bytes>>, Option<Sleep>) {
60+
) -> Result<(hyper::Response<Full<Bytes>>, Option<Sleep>), ErrorResponse> {
6061
debug!("Handling request.");
61-
let mut response_template: Option<ResponseTemplate> = None;
62+
let mut response_template: Option<_> = None;
6263
self.mocks.sort_by_key(|(m, _)| m.specification.priority);
6364
for (mock, mock_state) in &mut self.mocks {
6465
if *mock_state == MountedMockState::OutOfScope {
@@ -70,19 +71,22 @@ impl MountedMockSet {
7071
}
7172
}
7273
if let Some(response_template) = response_template {
73-
let delay = response_template.delay().map(sleep);
74-
(response_template.generate_response(), delay)
74+
match response_template {
75+
Ok(response_template) => {
76+
let delay = response_template.delay().map(sleep);
77+
Ok((response_template.generate_response(), delay))
78+
}
79+
Err(err) => Err(err),
80+
}
7581
} else {
7682
let mut msg = "Got unexpected request:\n".to_string();
7783
_ = request.print_with_limit(&mut msg, self.body_print_limit);
7884
debug!("{}", msg);
79-
(
80-
hyper::Response::builder()
81-
.status(hyper::StatusCode::NOT_FOUND)
82-
.body(Full::default())
83-
.unwrap(),
84-
None,
85-
)
85+
let not_found_response = hyper::Response::builder()
86+
.status(hyper::StatusCode::NOT_FOUND)
87+
.body(Full::default())
88+
.unwrap();
89+
Ok((not_found_response, None))
8690
}
8791
}
8892

src/mounted_mock.rs

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,9 @@ use std::sync::{atomic::AtomicBool, Arc};
22

33
use tokio::sync::Notify;
44

5-
use crate::{verification::VerificationReport, Match, Mock, Request, ResponseTemplate};
5+
use crate::{
6+
verification::VerificationReport, ErrorResponse, Match, Mock, Request, ResponseTemplate,
7+
};
68

79
/// Given the behaviour specification as a [`Mock`], keep track of runtime information
810
/// concerning this mock - e.g. how many times it matched on a incoming request.
@@ -80,7 +82,10 @@ impl MountedMock {
8082
}
8183
}
8284

83-
pub(crate) fn response_template(&self, request: &Request) -> ResponseTemplate {
85+
pub(crate) fn response_template(
86+
&self,
87+
request: &Request,
88+
) -> Result<ResponseTemplate, ErrorResponse> {
8489
self.specification.response_template(request)
8590
}
8691

src/respond.rs

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
use crate::{Request, ResponseTemplate};
1+
use crate::{ErrorResponse, Request, ResponseTemplate};
22

33
/// Anything that implements `Respond` can be used to reply to an incoming request when a
44
/// [`Mock`] is activated.
@@ -152,3 +152,18 @@ where
152152
(self)(request)
153153
}
154154
}
155+
156+
/// Like [`Respond`], but it only allows returning an error through a function.
157+
pub trait RespondErr: Send + Sync {
158+
fn respond_err(&self, request: &Request) -> ErrorResponse;
159+
}
160+
161+
impl<F, Err> RespondErr for F
162+
where
163+
F: Send + Sync + Fn(&Request) -> Err,
164+
Err: std::error::Error + Send + Sync + 'static,
165+
{
166+
fn respond_err(&self, request: &Request) -> ErrorResponse {
167+
Box::new((self)(request))
168+
}
169+
}

tests/mocks.rs

Lines changed: 61 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,13 @@ use futures::FutureExt;
22
use reqwest::StatusCode;
33
use serde::Serialize;
44
use serde_json::json;
5+
use std::fmt::{Display, Formatter};
6+
use std::io::ErrorKind;
7+
use std::iter;
58
use std::net::TcpStream;
69
use std::time::Duration;
710
use wiremock::matchers::{body_json, body_partial_json, method, path, PathExactMatcher};
8-
use wiremock::{Mock, MockServer, ResponseTemplate};
11+
use wiremock::{Mock, MockServer, Request, ResponseTemplate};
912

1013
#[async_std::test]
1114
async fn new_starts_the_server() {
@@ -390,3 +393,60 @@ async fn debug_prints_mock_server_variants() {
390393
format!("{:?}", bare_mock_server)
391394
);
392395
}
396+
397+
#[tokio::test]
398+
async fn io_err() {
399+
// Act
400+
let mock_server = MockServer::start().await;
401+
let mock = Mock::given(method("GET")).respond_with_err(|_: &Request| {
402+
std::io::Error::new(ErrorKind::ConnectionReset, "connection reset")
403+
});
404+
mock_server.register(mock).await;
405+
406+
// Assert
407+
let err = reqwest::get(&mock_server.uri()).await.unwrap_err();
408+
// We're skipping the original error since it can be either `error sending request` or
409+
// `error sending request for url (http://127.0.0.1:<port>/)`
410+
let actual_err: Vec<String> =
411+
iter::successors(std::error::Error::source(&err), |err| err.source())
412+
.map(|err| err.to_string())
413+
.collect();
414+
415+
let expected_err = vec![
416+
"client error (SendRequest)".to_string(),
417+
"connection closed before message completed".to_string(),
418+
];
419+
assert_eq!(actual_err, expected_err);
420+
}
421+
422+
#[tokio::test]
423+
async fn custom_err() {
424+
// Act
425+
#[derive(Debug)]
426+
struct CustomErr;
427+
impl Display for CustomErr {
428+
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
429+
f.write_str("custom error")
430+
}
431+
}
432+
impl std::error::Error for CustomErr {}
433+
434+
let mock_server = MockServer::start().await;
435+
let mock = Mock::given(method("GET")).respond_with_err(|_: &Request| CustomErr);
436+
mock_server.register(mock).await;
437+
438+
// Assert
439+
let err = reqwest::get(&mock_server.uri()).await.unwrap_err();
440+
// We're skipping the original error since it can be either `error sending request` or
441+
// `error sending request for url (http://127.0.0.1:<port>/)`
442+
let actual_err: Vec<String> =
443+
iter::successors(std::error::Error::source(&err), |err| err.source())
444+
.map(|err| err.to_string())
445+
.collect();
446+
447+
let expected_err = vec![
448+
"client error (SendRequest)".to_string(),
449+
"connection closed before message completed".to_string(),
450+
];
451+
assert_eq!(actual_err, expected_err);
452+
}

0 commit comments

Comments
 (0)