Added Url encoding and fixed bugs that cropped up in the process.

This commit is contained in:
Glenn Griffin 2019-12-10 10:15:27 -08:00
parent 1269db042b
commit 1922c99979
6 changed files with 107 additions and 10 deletions

View File

@ -18,6 +18,7 @@ regex = "1.3.1"
url = "2.1.0" url = "2.1.0"
serde_json = "1.0.44" serde_json = "1.0.44"
serde = "1.0.103" serde = "1.0.103"
serde_urlencoded = "0.6.1"
[dev-dependencies] [dev-dependencies]
pretty_env_logger = "0.3.1" pretty_env_logger = "0.3.1"

View File

@ -7,6 +7,7 @@ pub use crate::all_of;
pub use crate::any_of; pub use crate::any_of;
pub mod request; pub mod request;
pub mod response; pub mod response;
pub mod sequence;
pub trait Mapper<IN>: Send + fmt::Debug pub trait Mapper<IN>: Send + fmt::Debug
where where
@ -40,7 +41,10 @@ pub fn any() -> Any {
} }
#[derive(Debug)] #[derive(Debug)]
pub struct Any; pub struct Any;
impl<IN> Mapper<IN> for Any { impl<IN> Mapper<IN> for Any
where
IN: ?Sized,
{
type Out = bool; type Out = bool;
fn map(&mut self, _input: &IN) -> bool { fn map(&mut self, _input: &IN) -> bool {
@ -65,6 +69,23 @@ where
} }
} }
pub fn deref<C>(inner: C) -> Deref<C> {
Deref(inner)
}
#[derive(Debug)]
pub struct Deref<C>(C);
impl<C, IN> Mapper<IN> for Deref<C>
where
C: Mapper<IN::Target>,
IN: std::ops::Deref,
{
type Out = C::Out;
fn map(&mut self, input: &IN) -> C::Out {
self.0.map(input.deref())
}
}
pub trait IntoRegex { pub trait IntoRegex {
fn into_regex(self) -> regex::bytes::Regex; fn into_regex(self) -> regex::bytes::Regex;
} }
@ -173,15 +194,15 @@ where
} }
} }
pub fn uri_decoded<C>(inner: C) -> UriDecoded<C> pub fn url_decoded<C>(inner: C) -> UrlDecoded<C>
where where
C: Mapper<[(String, String)]>, C: Mapper<[(String, String)]>,
{ {
UriDecoded(inner) UrlDecoded(inner)
} }
#[derive(Debug)] #[derive(Debug)]
pub struct UriDecoded<C>(C); pub struct UrlDecoded<C>(C);
impl<IN, C> Mapper<IN> for UriDecoded<C> impl<IN, C> Mapper<IN> for UrlDecoded<C>
where where
IN: AsRef<[u8]> + ?Sized, IN: AsRef<[u8]> + ?Sized,
C: Mapper<[(String, String)]>, C: Mapper<[(String, String)]>,
@ -335,12 +356,12 @@ mod tests {
} }
#[test] #[test]
fn test_uri_decoded() { fn test_url_decoded() {
let expected = vec![ let expected = vec![
("key 1".to_owned(), "value 1".to_owned()), ("key 1".to_owned(), "value 1".to_owned()),
("key2".to_owned(), "".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") let req = http::Request::get("https://example.com/path?key%201=value%201&key2")
.body("") .body("")
.unwrap(); .unwrap();

View File

@ -88,12 +88,13 @@ pub fn body<C>(inner: C) -> Body<C> {
pub struct Body<C>(C); pub struct Body<C>(C);
impl<C, B> Mapper<hyper::Request<B>> for Body<C> impl<C, B> Mapper<hyper::Request<B>> for Body<C>
where where
C: Mapper<B>, B: ToOwned,
C: Mapper<B::Owned>,
{ {
type Out = C::Out; type Out = C::Out;
fn map(&mut self, input: &hyper::Request<B>) -> C::Out { fn map(&mut self, input: &hyper::Request<B>) -> C::Out {
self.0.map(input.body()) self.0.map(&input.body().to_owned())
} }
} }

View File

@ -46,6 +46,27 @@ impl Responder for JsonEncoded {
} }
} }
pub fn url_encoded<T>(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<Box<dyn Future<Output = http::Response<hyper::Body>> + Send>> {
async fn _respond(body: String) -> http::Response<hyper::Body> {
hyper::Response::builder()
.status(200)
.header("Content-Type", "application/x-www-form-urlencoded")
.body(body.into())
.unwrap()
}
Box::pin(_respond(self.0.clone()))
}
}
impl<B> Responder for hyper::Response<B> impl<B> Responder for hyper::Response<B>
where where
B: Clone + Into<hyper::Body> + Send + fmt::Debug, B: Clone + Into<hyper::Body> + Send + fmt::Debug,

View File

@ -109,6 +109,11 @@ impl Server {
/// all expectations leaving the server running in a clean state. /// all expectations leaving the server running in a clean state.
pub fn verify_and_clear(&mut self) { pub fn verify_and_clear(&mut self) {
let mut state = self.state.lock(); 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() { for expectation in state.expected.iter() {
let is_valid_cardinality = match &expectation.cardinality { let is_valid_cardinality = match &expectation.cardinality {
Times::AnyNumber => true, Times::AnyNumber => true,

View File

@ -84,6 +84,7 @@ async fn test_expectation_cardinality_exceeded() {
#[tokio::test] #[tokio::test]
async fn test_json() { async fn test_json() {
use bstr::B;
let _ = pretty_env_logger::try_init(); let _ = pretty_env_logger::try_init();
let my_data = serde_json::json!({ let my_data = serde_json::json!({
@ -103,11 +104,16 @@ async fn test_json() {
.respond_with(json_encoded(my_data.clone())), .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 client = hyper::Client::new();
let resp = read_response_body(client.get(server.url("/foo")).await.unwrap()).await; let resp = read_response_body(client.get(server.url("/foo")).await.unwrap()).await;
assert!(all_of![ assert!(all_of![
response::status_code(eq(200)), 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))), response::body(json_decoded(eq(my_data))),
] ]
.matches(&resp)); .matches(&resp));
@ -140,3 +146,45 @@ async fn test_cycle() {
let resp = read_response_body(client.get(server.url("/foo")).await.unwrap()).await; let resp = read_response_body(client.get(server.url("/foo")).await.unwrap()).await;
assert!(response::status_code(eq(404)).matches(&resp)); 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.
}