From 1922c9997926ed1cecb48e632ce586fc77b1a9b8 Mon Sep 17 00:00:00 2001 From: Glenn Griffin Date: Tue, 10 Dec 2019 10:15:27 -0800 Subject: [PATCH] Added Url encoding and fixed bugs that cropped up in the process. --- Cargo.toml | 1 + src/mappers.rs | 35 +++++++++++++++++++++++------ src/mappers/request.rs | 5 +++-- src/responders.rs | 21 ++++++++++++++++++ src/server.rs | 5 +++++ tests/tests.rs | 50 +++++++++++++++++++++++++++++++++++++++++- 6 files changed, 107 insertions(+), 10 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 99bbc9a..bdd3f5d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -18,6 +18,7 @@ regex = "1.3.1" url = "2.1.0" serde_json = "1.0.44" serde = "1.0.103" +serde_urlencoded = "0.6.1" [dev-dependencies] pretty_env_logger = "0.3.1" diff --git a/src/mappers.rs b/src/mappers.rs index e197c89..34daf7f 100644 --- a/src/mappers.rs +++ b/src/mappers.rs @@ -7,6 +7,7 @@ pub use crate::all_of; pub use crate::any_of; pub mod request; pub mod response; +pub mod sequence; pub trait Mapper: Send + fmt::Debug where @@ -40,7 +41,10 @@ pub fn any() -> Any { } #[derive(Debug)] pub struct Any; -impl Mapper for Any { +impl Mapper for Any +where + IN: ?Sized, +{ type Out = bool; fn map(&mut self, _input: &IN) -> bool { @@ -65,6 +69,23 @@ where } } +pub fn deref(inner: C) -> Deref { + Deref(inner) +} +#[derive(Debug)] +pub struct Deref(C); +impl Mapper for Deref +where + C: Mapper, + IN: std::ops::Deref, +{ + type Out = C::Out; + + fn map(&mut self, input: &IN) -> C::Out { + self.0.map(input.deref()) + } +} + pub trait IntoRegex { fn into_regex(self) -> regex::bytes::Regex; } @@ -173,15 +194,15 @@ where } } -pub fn uri_decoded(inner: C) -> UriDecoded +pub fn url_decoded(inner: C) -> UrlDecoded where C: Mapper<[(String, String)]>, { - UriDecoded(inner) + UrlDecoded(inner) } #[derive(Debug)] -pub struct UriDecoded(C); -impl Mapper for UriDecoded +pub struct UrlDecoded(C); +impl Mapper for UrlDecoded where IN: AsRef<[u8]> + ?Sized, C: Mapper<[(String, String)]>, @@ -335,12 +356,12 @@ mod tests { } #[test] - fn test_uri_decoded() { + fn test_url_decoded() { let expected = vec![ ("key 1".to_owned(), "value 1".to_owned()), ("key2".to_owned(), "".to_owned()), ]; - let mut c = request::query(uri_decoded(eq(expected))); + let mut c = request::query(url_decoded(eq(expected))); let req = http::Request::get("https://example.com/path?key%201=value%201&key2") .body("") .unwrap(); diff --git a/src/mappers/request.rs b/src/mappers/request.rs index cc0cb9a..957cdf7 100644 --- a/src/mappers/request.rs +++ b/src/mappers/request.rs @@ -88,12 +88,13 @@ pub fn body(inner: C) -> Body { pub struct Body(C); impl Mapper> for Body where - C: Mapper, + B: ToOwned, + C: Mapper, { type Out = C::Out; fn map(&mut self, input: &hyper::Request) -> C::Out { - self.0.map(input.body()) + self.0.map(&input.body().to_owned()) } } diff --git a/src/responders.rs b/src/responders.rs index 9121384..c252125 100644 --- a/src/responders.rs +++ b/src/responders.rs @@ -46,6 +46,27 @@ impl Responder for JsonEncoded { } } +pub fn url_encoded(data: T) -> impl Responder +where + T: serde::Serialize, +{ + UrlEncoded(serde_urlencoded::to_string(&data).unwrap()) +} +#[derive(Debug)] +pub struct UrlEncoded(String); +impl Responder for UrlEncoded { + fn respond(&mut self) -> Pin> + Send>> { + async fn _respond(body: String) -> http::Response { + hyper::Response::builder() + .status(200) + .header("Content-Type", "application/x-www-form-urlencoded") + .body(body.into()) + .unwrap() + } + Box::pin(_respond(self.0.clone())) + } +} + impl Responder for hyper::Response where B: Clone + Into + Send + fmt::Debug, diff --git a/src/server.rs b/src/server.rs index 5cec844..da16346 100644 --- a/src/server.rs +++ b/src/server.rs @@ -109,6 +109,11 @@ impl Server { /// all expectations leaving the server running in a clean state. pub fn verify_and_clear(&mut self) { let mut state = self.state.lock(); + if std::thread::panicking() { + // If the test is already panicking don't double panic on drop. + state.expected.clear(); + return; + } for expectation in state.expected.iter() { let is_valid_cardinality = match &expectation.cardinality { Times::AnyNumber => true, diff --git a/tests/tests.rs b/tests/tests.rs index bed2b34..474f6ab 100644 --- a/tests/tests.rs +++ b/tests/tests.rs @@ -84,6 +84,7 @@ async fn test_expectation_cardinality_exceeded() { #[tokio::test] async fn test_json() { + use bstr::B; let _ = pretty_env_logger::try_init(); let my_data = serde_json::json!({ @@ -103,11 +104,16 @@ async fn test_json() { .respond_with(json_encoded(my_data.clone())), ); - // Issue the GET /foo to the server and verify it returns a 200. + // Issue the GET /foo to the server and verify it returns a 200 with a json + // body matching my_data. let client = hyper::Client::new(); let resp = read_response_body(client.get(server.url("/foo")).await.unwrap()).await; assert!(all_of![ response::status_code(eq(200)), + response::headers(sequence::contains(( + deref(eq(B("content-type"))), + deref(eq(B("application/json"))), + ))), response::body(json_decoded(eq(my_data))), ] .matches(&resp)); @@ -140,3 +146,45 @@ async fn test_cycle() { let resp = read_response_body(client.get(server.url("/foo")).await.unwrap()).await; assert!(response::status_code(eq(404)).matches(&resp)); } + +#[tokio::test] +async fn test_url_encoded() { + use bstr::B; + let _ = pretty_env_logger::try_init(); + + // Setup a server to expect a single GET /foo request and respond with a + // json response. + let my_data = vec![("key", "value")]; + let server = httptest::Server::run(); + server.expect( + Expectation::matching(all_of![ + request::method(eq("GET")), + request::path(eq("/foo")), + request::query(url_decoded(sequence::contains(( + deref(eq("key")), + deref(eq("value")), + )))), + ]) + .times(Times::Exactly(1)) + .respond_with(url_encoded(my_data.clone())), + ); + + // Issue the GET /foo?key=value to the server and verify it returns a 200 with an + // application/x-www-form-urlencoded body of key=value. + let client = hyper::Client::new(); + let resp = read_response_body(client.get(server.url("/foo?key=value")).await.unwrap()).await; + assert!(all_of![ + response::status_code(eq(200)), + response::headers(sequence::contains(( + deref(eq(B("content-type"))), + deref(eq(B("application/x-www-form-urlencoded"))), + ))), + response::body(url_decoded(sequence::contains(( + deref(eq("key")), + deref(eq("value")) + )))), + ] + .matches(&resp)); + + // The Drop impl of the server will assert that all expectations were satisfied or else it will panic. +}