Compare commits

...

12 Commits

9 changed files with 754 additions and 244 deletions

View File

@@ -7,17 +7,18 @@ edition = "2018"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
hyper = {version = "=0.13.0-alpha.4", features = ["unstable-stream"]}
futures-preview = {version = "=0.3.0-alpha.19", features = ["std", "async-await"]}
tokio = "=0.2.0-alpha.6"
hyper = "0.13"
futures = "0.3"
tokio = { version = "0.2", features = ["macros"] }
crossbeam-channel = "0.4.0"
http = "0.1.18"
http = "0.2"
log = "0.4.8"
bstr = "0.2.8"
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"

View File

@@ -1,15 +1,218 @@
//! # httptest
//!
//! Provide convenient mechanism for testing http clients against a locally
//! running http server. The typical usage is as follows:
//!
//! * Start a server
//! * Configure the server by adding expectations
//! * Test your http client by making requests to the server
//! * On Drop the server verifies all expectations were met.
//!
//! ## Example Test
//!
//! ```
//! # async fn foo() {
//! use httptest::{Server, Expectation, Times, mappers::*, responders::*};
//! // Start a server running on a local ephemeral port.
//! let server = Server::run();
//! // Configure the server to expect a single GET /foo request and respond
//! // with a 200 status code.
//! server.expect(
//! Expectation::matching(all_of![
//! request::method(eq("GET")),
//! request::path(eq("/foo"))
//! ])
//! .times(Times::Exactly(1))
//! .respond_with(status_code(200)),
//! );
//!
//! // The server provides server.addr() that returns the address of the
//! // locally running server, or more conveniently provides a server.url() method
//! // that gives a fully formed http url to the provided path.
//! let url = server.url("/foo");
//! let client = hyper::Client::new();
//! // Issue the GET /foo to the server.
//! let resp = client.get(url).await.unwrap();
//!
//! // Use response matchers to assert the response has a 200 status code.
//! assert!(response::status_code(eq(200)).matches(&resp));
//!
//! // on Drop the server will assert all expectations have been met and will
//! // panic if not.
//! # }
//! ```
//!
//! # Server behavior
//!
//! The Server is started with [run()](struct.Server.html#method.run).
//!
//! The server will run in a background thread until it's dropped. Once dropped
//! it will assert that every configured expectation has been met or will panic.
//! You can also use [verify_and_clear()](struct.Server.html#method.verify_and_clear)
//! to assert and clear the expectations while keeping the server running.
//!
//! [addr()](struct.Server.html#method.addr) will return the address the
//! server is listening on.
//!
//! [url()](struct.Server.html#method.url) will
//! construct a fully formed http url to the path provided i.e.
//!
//! `server.url("/foo?key=value") == "https://<server_addr>/foo?key=value"`.
//!
//! # Defining Expecations
//!
//! Every expecation defines a request matcher, a defintion of the number of
//! times it's expected to be called, and what it should respond with.
//!
//! ### Expectation example
//!
//! ```
//! use httptest::{Expectation, mappers::*, responders::*, Times};
//!
//! // Define an Expectation that matches any request to path /foo, expects to
//! // receive at least 1 such request, and responds with a 200 response.
//! Expectation::matching(request::path(eq("/foo")))
//! .times(Times::AtLeast(1))
//! .respond_with(status_code(200));
//! ```
//!
//! ## Request Matchers
//!
//! Defining which request an expecation matches is done in a composable manner
//! using a series of traits. The core of which is
//! [Mapper](mappers/trait.Mapper.html). The `Mapper` trait is generic
//! over an input type, has an associated `Out` type, and defines a single method
//! `map` that converts from a shared reference of the input type to the `Out`
//! type.
//!
//! There's a specialized form of a Mapper where the `Out` type is a boolean.
//! Any `Mapper` that outputs a boolean value is considered a Matcher and
//! implements the [Matcher](mapper/trait.Matcher.html) trait as well. The
//! Matcher trait simply provides a `matches` method.
//!
//! A request matcher is any `Matcher` that takes accepts a
//! `hyper::Request<hyper::body::Bytes>` as input.
//!
//! With that understanding we can discuss how to easily define a request
//! matcher. There are a variety of pre-defined mappers within the `mappers`
//! module. These mappers can be composed together to define the values you want
//! to match. The mappers fall into two categories. Some of the mappers extract a
//! value from the input type and pass it to another mapper, other mappers accept
//! an input type and return a bool. These primitives provide an easy and
//! flexible way to define custom logic.
//!
//! ### Matcher examples
//!
//! ```
//! // pull all the predefined mappers into our namespace.
//! use httptest::mappers::*;
//!
//! // A mapper that returns true when the input equals "/foo"
//! let mut m = eq("/foo");
//!
//! // A mapper that returns true when the input matches the regex "(foo|bar).*"
//! let mut m = matches("(foo|bar).*");
//!
//! // A request matcher that matches a request to path "/foo"
//! let mut m = request::path(eq("/foo"));
//!
//! // A request matcher that matches a POST request
//! let mut m = request::method(eq("POST"));
//!
//! // A request matcher that matches a POST with a path that matches the regex 'foo.*'
//! let mut m = all_of![
//! request::method(eq("POST")),
//! request::path(matches("foo.*")),
//! ];
//!
//! # // Allow type inference to determine the request type.
//! # m.map(&hyper::Request::get("/").body("").unwrap());
//! ```
//!
//! ## Times
//!
//! Each expectation defines how many times a matching request is expected to
//! be received. The [Times](enum.Times.html) enum defines the possibility.
//! `Times::Exactly(1)` is the default value of an `Expectation` if one is not
//! specified with the
//! [times()](struct.ExpectationBuilder.html#method.times) method.
//!
//! The server will respond to any requests that violate the times request with
//! a 500 status code and the server will subsequently panic on Drop.
//!
//! ## Responder
//!
//! Responders define how the server will respond to a matched request. There
//! are a number of implemented responders within the responders module. In
//! addition to the predefined responders you can provide any
//! `hyper::Response` with a body that can be cloned or implement your own
//! Responder.
//!
//! ## Responder example
//!
//! ```
//! use httptest::responders::*;
//!
//! // respond with a successful 200 status code.
//! status_code(200);
//!
//! // respond with a 404 page not found.
//! status_code(404);
//!
//! // respond with a json encoded body.
//! json_encoded(serde_json::json!({
//! "my_key": 100,
//! "my_key2": [1, 2, "foo", 99],
//! }));
//!
//! // alternate between responding with a 200 and a 404.
//! cycle![
//! status_code(200),
//! status_code(404),
//! ];
//!
//! ```
#![deny(missing_docs)]
/// true if all the provided matchers return true.
///
/// The macro exists to conveniently box a list of mappers and put them into a
/// `Vec<Box<dyn Mapper>>`. The translation is:
///
/// `all_of![a, b] => all_of(vec![Box::new(a), Box::new(b)])`
#[macro_export]
macro_rules! all_of {
($($x:expr),*) => ($crate::mappers::all_of($crate::vec_of_boxes![$($x),*]));
($($x:expr,)*) => ($crate::all_of![$($x),*]);
}
/// true if any of the provided matchers return true.
///
/// The macro exists to conveniently box a list of mappers and put them into a
/// `Vec<Box<dyn Mapper>>`. The translation is:
///
/// `any_of![a, b] => any_of(vec![Box::new(a), Box::new(b)])`
#[macro_export]
macro_rules! any_of {
($($x:expr),*) => ($crate::mappers::any_of($crate::vec_of_boxes![$($x),*]));
($($x:expr,)*) => ($crate::any_of![$($x),*]);
}
/// a Responder that cycles through a list of responses.
///
/// The macro exists to conveniently box a list of responders and put them into a
/// `Vec<Box<dyn Responder>>`. The translation is:
///
/// `cycle![a, b] => cycle(vec![Box::new(a), Box::new(b)])`
#[macro_export]
macro_rules! cycle {
($($x:expr),*) => ($crate::responders::cycle($crate::vec_of_boxes![$($x),*]));
($($x:expr,)*) => ($crate::cycle![$($x),*]);
}
// hidden from docs because it's an implementation detail of the above macros.
#[doc(hidden)]
#[macro_export]
macro_rules! vec_of_boxes {
($($x:expr),*) => (std::vec![$(std::boxed::Box::new($x)),*]);
@@ -18,12 +221,6 @@ macro_rules! vec_of_boxes {
pub mod mappers;
pub mod responders;
pub mod server;
mod server;
pub type FullRequest = hyper::Request<Vec<u8>>;
pub type FullResponse = hyper::Response<Vec<u8>>;
pub use mappers::Matcher;
pub use server::Expectation;
pub use server::Server;
pub use server::Times;
pub use server::{Expectation, ExpectationBuilder, Server, Times};

View File

@@ -1,30 +1,52 @@
//! Mapper implementations.
//!
//! This module contains mappers for composing a set of operations. The result
//! of the composition usually results in a boolean. Any `Mapper` that results in a
//! boolean value also implemens `Matcher`.
use std::borrow::Borrow;
use std::fmt;
use std::marker::PhantomData;
// import the any_of and all_of macros from crate root so they are accessible if
// people glob import this module.
#[doc(inline)]
pub use crate::all_of;
#[doc(inline)]
pub use crate::any_of;
pub mod request;
pub mod response;
pub mod sequence;
/// The core trait. Defines how an input value should be turned into an output
/// value. This allows for a flexible pattern of composition where two or more
/// mappers are chained together to form a readable and flexible manipulation.
///
/// There is a special case of a Mapper that outputs a bool that is called a
/// Matcher.
pub trait Mapper<IN>: Send + fmt::Debug
where
IN: ?Sized,
{
/// The output type.
type Out;
/// Map an input to output.
fn map(&mut self, input: &IN) -> Self::Out;
}
// Matcher is just a special case of Mapper that returns a boolean. Simply
// provides the `matches` method rather than `map` as that reads a little
// better.
/// Matcher is just a special case of Mapper that returns a boolean. It simply
/// provides the `matches` method rather than `map` as that reads a little
/// better.
///
/// There is a blanket implementation for all Mappers that output bool values.
/// You should never implement Matcher yourself, instead implement Mapper with a
/// bool Out parameter.
pub trait Matcher<IN>: Send + fmt::Debug
where
IN: ?Sized,
{
/// true if the input matches.
fn matches(&mut self, input: &IN) -> bool;
}
impl<T, IN> Matcher<IN> for T
@@ -36,12 +58,17 @@ where
}
}
pub fn any<IN>() -> impl Mapper<IN, Out = bool> {
/// Always true.
pub fn any() -> Any {
Any
}
/// The `Any` mapper returned by [any()](fn.any.html)
#[derive(Debug)]
pub struct Any;
impl<IN> Mapper<IN> for Any {
impl<IN> Mapper<IN> for Any
where
IN: ?Sized,
{
type Out = bool;
fn map(&mut self, _input: &IN) -> bool {
@@ -49,35 +76,11 @@ impl<IN> Mapper<IN> for Any {
}
}
pub fn contains<T, IN>(value: T) -> impl Mapper<IN, Out = bool>
where
T: AsRef<[u8]> + fmt::Debug + Send,
IN: AsRef<[u8]> + ?Sized,
{
Contains(value)
}
#[derive(Debug)]
pub struct Contains<T>(T);
impl<IN, T> Mapper<IN> for Contains<T>
where
T: AsRef<[u8]> + fmt::Debug + Send,
IN: AsRef<[u8]> + ?Sized,
{
type Out = bool;
fn map(&mut self, input: &IN) -> bool {
use bstr::ByteSlice;
input.as_ref().contains_str(self.0.as_ref())
}
}
pub fn eq<T, IN>(value: T) -> impl Mapper<IN, Out = bool>
where
T: Borrow<IN> + fmt::Debug + Send,
IN: PartialEq + ?Sized,
{
/// true if the input is equal to value.
pub fn eq<T>(value: T) -> Eq<T> {
Eq(value)
}
/// The `Eq` mapper returned by [eq()](fn.eq.html)
#[derive(Debug)]
pub struct Eq<T>(T);
impl<IN, T> Mapper<IN> for Eq<T>
@@ -92,13 +95,59 @@ where
}
}
pub fn matches<IN>(value: &str) -> impl Mapper<IN, Out = bool>
where
IN: AsRef<[u8]> + ?Sized,
{
let regex = regex::bytes::Regex::new(value).expect("failed to create regex");
Matches(regex)
/// Call Deref::deref() on the input and pass it to the next mapper.
pub fn deref<M>(inner: M) -> Deref<M> {
Deref(inner)
}
/// The `Deref` mapper returned by [deref()](fn.deref.html)
#[derive(Debug)]
pub struct Deref<M>(M);
impl<M, IN> Mapper<IN> for Deref<M>
where
M: Mapper<IN::Target>,
IN: std::ops::Deref,
{
type Out = M::Out;
fn map(&mut self, input: &IN) -> M::Out {
self.0.map(input.deref())
}
}
/// Create a regex.
///
/// This trait may panic if the regex failed to build.
pub trait IntoRegex {
/// turn self into a regex.
fn into_regex(self) -> regex::bytes::Regex;
}
impl IntoRegex for &str {
fn into_regex(self) -> regex::bytes::Regex {
regex::bytes::Regex::new(self).expect("failed to create regex")
}
}
impl IntoRegex for String {
fn into_regex(self) -> regex::bytes::Regex {
regex::bytes::Regex::new(&self).expect("failed to create regex")
}
}
impl IntoRegex for &mut regex::bytes::RegexBuilder {
fn into_regex(self) -> regex::bytes::Regex {
self.build().expect("failed to create regex")
}
}
impl IntoRegex for regex::bytes::Regex {
fn into_regex(self) -> regex::bytes::Regex {
self
}
}
/// true if the input matches the regex provided.
pub fn matches(value: impl IntoRegex) -> Matches {
//let regex = regex::bytes::Regex::new(value).expect("failed to create regex");
Matches(value.into_regex())
}
/// The `Matches` mapper returned by [matches()](fn.matches.html)
#[derive(Debug)]
pub struct Matches(regex::bytes::Regex);
impl<IN> Mapper<IN> for Matches
@@ -112,19 +161,15 @@ where
}
}
pub fn not<C, IN>(inner: C) -> impl Mapper<IN, Out = bool>
where
C: Mapper<IN, Out = bool>,
IN: ?Sized,
{
Not(inner, PhantomData)
/// invert the result of the inner mapper.
pub fn not<M>(inner: M) -> Not<M> {
Not(inner)
}
pub struct Not<C, IN>(C, PhantomData<fn(IN)>)
/// The `Not` mapper returned by [not()](fn.not.html)
pub struct Not<M>(M);
impl<M, IN> Mapper<IN> for Not<M>
where
IN: ?Sized;
impl<C, IN> Mapper<IN> for Not<C, IN>
where
C: Mapper<IN, Out = bool>,
M: Mapper<IN, Out = bool>,
IN: ?Sized,
{
type Out = bool;
@@ -133,23 +178,25 @@ where
!self.0.map(input)
}
}
impl<C, IN> fmt::Debug for Not<C, IN>
impl<M> fmt::Debug for Not<M>
where
C: Mapper<IN, Out = bool>,
IN: ?Sized,
M: fmt::Debug,
{
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "Not({:?})", &self.0)
}
}
pub fn all_of<IN>(inner: Vec<Box<dyn Mapper<IN, Out = bool>>>) -> impl Mapper<IN, Out = bool>
/// true if all the provided matchers return true. See the `all_of!` macro for
/// convenient usage.
pub fn all_of<IN>(inner: Vec<Box<dyn Mapper<IN, Out = bool>>>) -> AllOf<IN>
where
IN: fmt::Debug + ?Sized,
{
AllOf(inner)
}
/// The `AllOf` mapper returned by [all_of()](fn.all_of.html)
#[derive(Debug)]
pub struct AllOf<IN>(Vec<Box<dyn Mapper<IN, Out = bool>>>)
where
@@ -165,12 +212,15 @@ where
}
}
pub fn any_of<IN>(inner: Vec<Box<dyn Mapper<IN, Out = bool>>>) -> impl Mapper<IN, Out = bool>
/// true if any of the provided matchers returns true. See the `any_of!` macro
/// for convenient usage.
pub fn any_of<IN>(inner: Vec<Box<dyn Mapper<IN, Out = bool>>>) -> AnyOf<IN>
where
IN: fmt::Debug + ?Sized,
{
AnyOf(inner)
}
/// The `AnyOf` mapper returned by [any_of()](fn.any_of.html)
#[derive(Debug)]
pub struct AnyOf<IN>(Vec<Box<dyn Mapper<IN, Out = bool>>>)
where
@@ -186,23 +236,21 @@ where
}
}
pub fn uri_decoded<IN, C>(inner: C) -> impl Mapper<IN, Out = C::Out>
where
IN: AsRef<[u8]> + ?Sized,
C: Mapper<[(String, String)]>,
{
UriDecoded(inner)
/// url decode the input and pass the resulting slice of key-value pairs to the next mapper.
pub fn url_decoded<M>(inner: M) -> UrlDecoded<M> {
UrlDecoded(inner)
}
/// The `UrlDecoded` mapper returned by [url_decoded()](fn.url_decoded.html)
#[derive(Debug)]
pub struct UriDecoded<C>(C);
impl<IN, C> Mapper<IN> for UriDecoded<C>
pub struct UrlDecoded<M>(M);
impl<IN, M> Mapper<IN> for UrlDecoded<M>
where
IN: AsRef<[u8]> + ?Sized,
C: Mapper<[(String, String)]>,
M: Mapper<[(String, String)]>,
{
type Out = C::Out;
type Out = M::Out;
fn map(&mut self, input: &IN) -> C::Out {
fn map(&mut self, input: &IN) -> M::Out {
let decoded: Vec<(String, String)> = url::form_urlencoded::parse(input.as_ref())
.into_owned()
.collect();
@@ -210,63 +258,102 @@ where
}
}
pub fn json_decoded<IN, C>(inner: C) -> impl Mapper<IN, Out = C::Out>
where
IN: AsRef<[u8]> + ?Sized,
C: Mapper<serde_json::Value>,
{
/// json decode the input and pass the resulting serde_json::Value to the next
/// mapper.
///
/// If the input can't be decoded a serde_json::Value::Null is passed to the next
/// mapper.
pub fn json_decoded<M>(inner: M) -> JsonDecoded<M> {
JsonDecoded(inner)
}
/// The `JsonDecoded` mapper returned by [json_decoded()](fn.json_decoded.html)
#[derive(Debug)]
pub struct JsonDecoded<C>(C);
impl<IN, C> Mapper<IN> for JsonDecoded<C>
pub struct JsonDecoded<M>(M);
impl<IN, M> Mapper<IN> for JsonDecoded<M>
where
IN: AsRef<[u8]> + ?Sized,
C: Mapper<serde_json::Value>,
M: Mapper<serde_json::Value>,
{
type Out = C::Out;
type Out = M::Out;
fn map(&mut self, input: &IN) -> C::Out {
fn map(&mut self, input: &IN) -> M::Out {
let json_value: serde_json::Value =
serde_json::from_slice(input.as_ref()).unwrap_or(serde_json::Value::Null);
self.0.map(&json_value)
}
}
pub fn lowercase<IN, C>(inner: C) -> impl Mapper<IN, Out = C::Out>
where
IN: AsRef<[u8]> + ?Sized,
C: Mapper<[u8]>,
{
/// lowercase the input and pass it to the next mapper.
pub fn lowercase<M>(inner: M) -> Lowercase<M> {
Lowercase(inner)
}
/// The `Lowercase` mapper returned by [lowercase()](fn.lowercase.html)
#[derive(Debug)]
pub struct Lowercase<C>(C);
impl<IN, C> Mapper<IN> for Lowercase<C>
pub struct Lowercase<M>(M);
impl<IN, M> Mapper<IN> for Lowercase<M>
where
IN: AsRef<[u8]> + ?Sized,
C: Mapper<[u8]>,
M: Mapper<[u8]>,
{
type Out = C::Out;
type Out = M::Out;
fn map(&mut self, input: &IN) -> C::Out {
fn map(&mut self, input: &IN) -> M::Out {
use bstr::ByteSlice;
self.0.map(&input.as_ref().to_lowercase())
}
}
/// pass the input to the provided `Fn(T) -> bool` and return the result.
pub fn map_fn<F>(f: F) -> MapFn<F> {
MapFn(f)
}
/// The `MapFn` mapper returned by [map_fn()](fn.map_fn.html)
pub struct MapFn<F>(F);
impl<IN, F> Mapper<IN> for MapFn<F>
where
F: Fn(&IN) -> bool + Send,
{
type Out = bool;
fn map(&mut self, input: &IN) -> bool {
self.0(input)
}
}
impl<F> fmt::Debug for MapFn<F> {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "MapFn")
}
}
/// inspect the input and pass it to the next mapper.
///
/// This logs the value as it passes it to the next mapper unchanged. Can be
/// useful when troubleshooting why a matcher may not be working as intended.
pub fn inspect<M>(inner: M) -> Inspect<M> {
Inspect(inner)
}
/// The `Inspect` mapper returned by [inspect()](fn.inspect.html)
#[derive(Debug)]
pub struct Inspect<M>(M);
impl<IN, M> Mapper<IN> for Inspect<M>
where
IN: fmt::Debug + ?Sized,
M: Mapper<IN>,
M::Out: fmt::Debug,
{
type Out = M::Out;
fn map(&mut self, input: &IN) -> M::Out {
let output = self.0.map(input);
log::debug!("{:?}.map({:?}) == {:?}", self.0, input, output);
output
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_contains() {
let mut c = contains("foo");
assert_eq!(true, c.map("foobar"));
assert_eq!(true, c.map("bazfoobar"));
assert_eq!(false, c.map("bar"));
}
#[test]
fn test_eq() {
let mut c = eq("foo");
@@ -278,11 +365,36 @@ mod tests {
#[test]
fn test_matches() {
// regex from str
let mut c = matches(r#"^foo\d*bar$"#);
assert_eq!(true, c.map("foobar"));
assert_eq!(true, c.map("foo99bar"));
assert_eq!(false, c.map("foo99barz"));
assert_eq!(false, c.map("bat"));
// regex from String
let mut c = matches(r#"^foo\d*bar$"#.to_owned());
assert_eq!(true, c.map("foobar"));
assert_eq!(true, c.map("foo99bar"));
assert_eq!(false, c.map("foo99barz"));
assert_eq!(false, c.map("bat"));
// regex from RegexBuilder
let mut c = matches(regex::bytes::RegexBuilder::new("foobar").case_insensitive(true));
assert_eq!(true, c.map("foobar"));
assert_eq!(true, c.map("FOOBAR"));
assert_eq!(false, c.map("FOO99BAR"));
// regex from Regex
let mut c = matches(
regex::bytes::RegexBuilder::new("foobar")
.case_insensitive(true)
.build()
.unwrap(),
);
assert_eq!(true, c.map("foobar"));
assert_eq!(true, c.map("FOOBAR"));
assert_eq!(false, c.map("FOO99BAR"));
}
#[test]
@@ -296,7 +408,7 @@ mod tests {
#[test]
fn test_all_of() {
let mut c = all_of![contains("foo"), contains("bar")];
let mut c = all_of![matches("foo"), matches("bar")];
assert_eq!(true, c.map("foobar"));
assert_eq!(true, c.map("barfoo"));
assert_eq!(false, c.map("foo"));
@@ -305,7 +417,7 @@ mod tests {
#[test]
fn test_any_of() {
let mut c = any_of![contains("foo"), contains("bar")];
let mut c = any_of![matches("foo"), matches("bar")];
assert_eq!(true, c.map("foobar"));
assert_eq!(true, c.map("barfoo"));
assert_eq!(true, c.map("foo"));
@@ -314,14 +426,14 @@ 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 req = http::Request::get("https://example.com/path?key%201=value%201&key2")
.body(Vec::new())
let mut c = request::query(url_decoded(eq(expected)));
let req = hyper::Request::get("https://example.com/path?key%201=value%201&key2")
.body("")
.unwrap();
assert_eq!(true, c.map(&req));
@@ -340,10 +452,27 @@ mod tests {
#[test]
fn test_lowercase() {
let mut c = lowercase(contains("foo"));
let mut c = lowercase(matches("foo"));
assert_eq!(true, c.map("FOO"));
assert_eq!(true, c.map("FoOBar"));
assert_eq!(true, c.map("foobar"));
assert_eq!(false, c.map("bar"));
}
#[test]
fn test_fn_mapper() {
let mut c = map_fn(|input: &u64| input % 2 == 0);
assert_eq!(true, c.map(&6));
assert_eq!(true, c.map(&20));
assert_eq!(true, c.map(&0));
assert_eq!(false, c.map(&11));
}
#[test]
fn test_inspect() {
let _ = pretty_env_logger::try_init();
let mut c = inspect(lowercase(matches("^foobar$")));
assert_eq!(true, c.map("Foobar"));
assert_eq!(false, c.map("Foobar1"));
}
}

View File

@@ -1,78 +1,76 @@
use super::Mapper;
use crate::FullRequest;
//! Mappers that extract information from HTTP requests.
pub fn method<C>(inner: C) -> impl Mapper<FullRequest, Out = C::Out>
where
C: Mapper<str>,
{
use super::Mapper;
/// Extract the method from the HTTP request and pass it to the next mapper.
pub fn method<M>(inner: M) -> Method<M> {
Method(inner)
}
/// The `Method` mapper returned by [method()](fn.method.html)
#[derive(Debug)]
pub struct Method<C>(C);
impl<C> Mapper<FullRequest> for Method<C>
pub struct Method<M>(M);
impl<M, B> Mapper<hyper::Request<B>> for Method<M>
where
C: Mapper<str>,
M: Mapper<str>,
{
type Out = C::Out;
type Out = M::Out;
fn map(&mut self, input: &FullRequest) -> C::Out {
fn map(&mut self, input: &hyper::Request<B>) -> M::Out {
self.0.map(input.method().as_str())
}
}
pub fn path<C>(inner: C) -> impl Mapper<FullRequest, Out = C::Out>
where
C: Mapper<str>,
{
/// Extract the path from the HTTP request and pass it to the next mapper.
pub fn path<M>(inner: M) -> Path<M> {
Path(inner)
}
/// The `Path` mapper returned by [path()](fn.path.html)
#[derive(Debug)]
pub struct Path<C>(C);
impl<C> Mapper<FullRequest> for Path<C>
pub struct Path<M>(M);
impl<M, B> Mapper<hyper::Request<B>> for Path<M>
where
C: Mapper<str>,
M: Mapper<str>,
{
type Out = C::Out;
type Out = M::Out;
fn map(&mut self, input: &FullRequest) -> C::Out {
fn map(&mut self, input: &hyper::Request<B>) -> M::Out {
self.0.map(input.uri().path())
}
}
pub fn query<C>(inner: C) -> impl Mapper<FullRequest, Out = C::Out>
where
C: Mapper<str>,
{
/// Extract the query from the HTTP request and pass it to the next mapper.
pub fn query<M>(inner: M) -> Query<M> {
Query(inner)
}
/// The `Query` mapper returned by [query()](fn.query.html)
#[derive(Debug)]
pub struct Query<C>(C);
impl<C> Mapper<FullRequest> for Query<C>
pub struct Query<M>(M);
impl<M, B> Mapper<hyper::Request<B>> for Query<M>
where
C: Mapper<str>,
M: Mapper<str>,
{
type Out = C::Out;
type Out = M::Out;
fn map(&mut self, input: &FullRequest) -> C::Out {
fn map(&mut self, input: &hyper::Request<B>) -> M::Out {
self.0.map(input.uri().query().unwrap_or(""))
}
}
pub fn headers<C>(inner: C) -> impl Mapper<FullRequest, Out = C::Out>
where
C: Mapper<[(Vec<u8>, Vec<u8>)]>,
{
/// Extract the headers from the HTTP request and pass the sequence to the next
/// mapper.
pub fn headers<M>(inner: M) -> Headers<M> {
Headers(inner)
}
/// The `Headers` mapper returned by [headers()](fn.headers.html)
#[derive(Debug)]
pub struct Headers<C>(C);
impl<C> Mapper<FullRequest> for Headers<C>
pub struct Headers<M>(M);
impl<M, B> Mapper<hyper::Request<B>> for Headers<M>
where
C: Mapper<[(Vec<u8>, Vec<u8>)]>,
M: Mapper<[(Vec<u8>, Vec<u8>)]>,
{
type Out = C::Out;
type Out = M::Out;
fn map(&mut self, input: &FullRequest) -> C::Out {
fn map(&mut self, input: &hyper::Request<B>) -> M::Out {
let headers: Vec<(Vec<u8>, Vec<u8>)> = input
.headers()
.iter()
@@ -82,22 +80,22 @@ where
}
}
pub fn body<C>(inner: C) -> impl Mapper<FullRequest, Out = C::Out>
where
C: Mapper<[u8]>,
{
/// Extract the body from the HTTP request and pass it to the next mapper.
pub fn body<M>(inner: M) -> Body<M> {
Body(inner)
}
/// The `Body` mapper returned by [body()](fn.body.html)
#[derive(Debug)]
pub struct Body<C>(C);
impl<C> Mapper<FullRequest> for Body<C>
pub struct Body<M>(M);
impl<M, B> Mapper<hyper::Request<B>> for Body<M>
where
C: Mapper<[u8]>,
B: ToOwned,
M: Mapper<B::Owned>,
{
type Out = C::Out;
type Out = M::Out;
fn map(&mut self, input: &FullRequest) -> C::Out {
self.0.map(input.body())
fn map(&mut self, input: &hyper::Request<B>) -> M::Out {
self.0.map(&input.body().to_owned())
}
}
@@ -109,12 +107,12 @@ mod tests {
#[test]
fn test_path() {
let req = hyper::Request::get("https://example.com/foo")
.body(Vec::new())
.body("")
.unwrap();
assert!(path(eq("/foo")).map(&req));
let req = hyper::Request::get("https://example.com/foobar")
.body(Vec::new())
.body("")
.unwrap();
assert!(path(eq("/foobar")).map(&req))
}
@@ -122,11 +120,11 @@ mod tests {
#[test]
fn test_query() {
let req = hyper::Request::get("https://example.com/path?foo=bar&baz=bat")
.body(Vec::new())
.body("")
.unwrap();
assert!(query(eq("foo=bar&baz=bat")).map(&req));
let req = hyper::Request::get("https://example.com/path?search=1")
.body(Vec::new())
.body("")
.unwrap();
assert!(query(eq("search=1")).map(&req));
}
@@ -134,11 +132,11 @@ mod tests {
#[test]
fn test_method() {
let req = hyper::Request::get("https://example.com/foo")
.body(Vec::new())
.body("")
.unwrap();
assert!(method(eq("GET")).map(&req));
let req = hyper::Request::post("https://example.com/foobar")
.body(Vec::new())
.body("")
.unwrap();
assert!(method(eq("POST")).map(&req));
}
@@ -150,7 +148,7 @@ mod tests {
(Vec::from("content-length"), Vec::from("101")),
];
let mut req = hyper::Request::get("https://example.com/path?key%201=value%201&key2")
.body(Vec::new())
.body("")
.unwrap();
req.headers_mut().extend(vec![
(
@@ -168,10 +166,9 @@ mod tests {
#[test]
fn test_body() {
use bstr::{ByteVec, B};
let req = hyper::Request::get("https://example.com/foo")
.body(Vec::from_slice("my request body"))
.body("my request body")
.unwrap();
assert!(body(eq(B("my request body"))).map(&req));
assert!(body(eq("my request body")).map(&req));
}
}

View File

@@ -1,40 +1,40 @@
use super::Mapper;
use crate::FullResponse;
//! Mappers that extract information from HTTP responses.
pub fn status_code<C>(inner: C) -> impl Mapper<FullResponse, Out = C::Out>
where
C: Mapper<u16>,
{
use super::Mapper;
/// Extract the status code from the HTTP response and pass it to the next mapper.
pub fn status_code<M>(inner: M) -> StatusCode<M> {
StatusCode(inner)
}
/// The `StatusCode` mapper returned by [status_code()](fn.status_code.html)
#[derive(Debug)]
pub struct StatusCode<C>(C);
impl<C> Mapper<FullResponse> for StatusCode<C>
pub struct StatusCode<M>(M);
impl<M, B> Mapper<hyper::Response<B>> for StatusCode<M>
where
C: Mapper<u16>,
M: Mapper<u16>,
{
type Out = C::Out;
type Out = M::Out;
fn map(&mut self, input: &FullResponse) -> C::Out {
fn map(&mut self, input: &hyper::Response<B>) -> M::Out {
self.0.map(&input.status().as_u16())
}
}
pub fn headers<C>(inner: C) -> impl Mapper<FullResponse, Out = C::Out>
where
C: Mapper<[(Vec<u8>, Vec<u8>)]>,
{
/// Extract the headers from the HTTP response and pass the sequence to the next
/// mapper.
pub fn headers<M>(inner: M) -> Headers<M> {
Headers(inner)
}
/// The `Headers` mapper returned by [headers()](fn.headers.html)
#[derive(Debug)]
pub struct Headers<C>(C);
impl<C> Mapper<FullResponse> for Headers<C>
pub struct Headers<M>(M);
impl<M, B> Mapper<hyper::Response<B>> for Headers<M>
where
C: Mapper<[(Vec<u8>, Vec<u8>)]>,
M: Mapper<[(Vec<u8>, Vec<u8>)]>,
{
type Out = C::Out;
type Out = M::Out;
fn map(&mut self, input: &FullResponse) -> C::Out {
fn map(&mut self, input: &hyper::Response<B>) -> M::Out {
let headers: Vec<(Vec<u8>, Vec<u8>)> = input
.headers()
.iter()
@@ -44,21 +44,20 @@ where
}
}
pub fn body<C>(inner: C) -> impl Mapper<FullResponse, Out = C::Out>
where
C: Mapper<[u8]>,
{
/// Extract the body from the HTTP response and pass it to the next mapper.
pub fn body<M>(inner: M) -> Body<M> {
Body(inner)
}
/// The `Body` mapper returned by [body()](fn.body.html)
#[derive(Debug)]
pub struct Body<C>(C);
impl<C> Mapper<FullResponse> for Body<C>
pub struct Body<M>(M);
impl<M, B> Mapper<hyper::Response<B>> for Body<M>
where
C: Mapper<[u8]>,
M: Mapper<B>,
{
type Out = C::Out;
type Out = M::Out;
fn map(&mut self, input: &FullResponse) -> C::Out {
fn map(&mut self, input: &hyper::Response<B>) -> M::Out {
self.0.map(input.body())
}
}
@@ -72,13 +71,13 @@ mod tests {
fn test_status_code() {
let resp = hyper::Response::builder()
.status(hyper::StatusCode::NOT_FOUND)
.body(Vec::new())
.body("")
.unwrap();
assert!(status_code(eq(404)).map(&resp));
let resp = hyper::Response::builder()
.status(hyper::StatusCode::OK)
.body(Vec::new())
.body("")
.unwrap();
assert!(status_code(eq(200)).map(&resp));
}
@@ -92,7 +91,7 @@ mod tests {
let resp = hyper::Response::builder()
.header("host", "example.com")
.header("content-length", 101)
.body(Vec::new())
.body("")
.unwrap();
assert!(headers(eq(expected)).map(&resp));
@@ -100,10 +99,7 @@ mod tests {
#[test]
fn test_body() {
use bstr::{ByteVec, B};
let resp = hyper::Response::builder()
.body(Vec::from_slice("my request body"))
.unwrap();
assert!(body(eq(B("my request body"))).map(&resp));
let resp = hyper::Response::builder().body("my request body").unwrap();
assert!(body(eq("my request body")).map(&resp));
}
}

61
src/mappers/sequence.rs Normal file
View File

@@ -0,0 +1,61 @@
//! Mappers that handle sequences of items.
use super::Mapper;
/// true if the provided mapper returns true for any of the elements in the
/// sequence.
pub fn contains<M>(inner: M) -> Contains<M> {
Contains(inner)
}
/// The `Contains` mapper returned by [contains()](fn.contains.html)
#[derive(Debug)]
pub struct Contains<M>(M);
impl<M, E> Mapper<[E]> for Contains<M>
where
M: Mapper<E, Out = bool>,
{
type Out = bool;
fn map(&mut self, input: &[E]) -> bool {
for elem in input {
if self.0.map(elem) {
return true;
}
}
false
}
}
impl<K, V, KMapper, VMapper> Mapper<(K, V)> for (KMapper, VMapper)
where
KMapper: Mapper<K, Out = bool>,
VMapper: Mapper<V, Out = bool>,
{
type Out = bool;
fn map(&mut self, input: &(K, V)) -> bool {
self.0.map(&input.0) && self.1.map(&input.1)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::mappers::*;
#[test]
fn test_contains() {
let mut c = contains(eq(100));
assert_eq!(true, c.map(vec![100, 200, 300].as_slice()));
assert_eq!(false, c.map(vec![99, 200, 300].as_slice()));
}
#[test]
fn test_tuple() {
let kv = ("key1", "value1");
assert_eq!(true, (matches("key1"), any()).map(&kv));
assert_eq!(true, (matches("key1"), matches("value1")).map(&kv));
assert_eq!(false, (matches("key1"), matches("value2")).map(&kv));
assert_eq!(false, (matches("key2"), matches("value1")).map(&kv));
}
}

View File

@@ -1,19 +1,31 @@
//! Responder implementations.
//!
//! Reponders determine how the server will respond.
use std::fmt;
use std::future::Future;
use std::pin::Pin;
// import the cycle macro so that it's available if people glob import this module.
#[doc(inline)]
pub use crate::cycle;
/// Respond with an HTTP response.
pub trait Responder: Send + fmt::Debug {
fn respond(&mut self) -> Pin<Box<dyn Future<Output = http::Response<hyper::Body>> + Send>>;
/// Return a future that outputs an HTTP response.
fn respond(&mut self) -> Pin<Box<dyn Future<Output = hyper::Response<hyper::Body>> + Send>>;
}
/// respond with the provided status code.
pub fn status_code(code: u16) -> impl Responder {
StatusCode(code)
}
/// The `StatusCode` responder returned by [status_code()](fn.status_code.html)
#[derive(Debug)]
pub struct StatusCode(u16);
impl Responder for StatusCode {
fn respond(&mut self) -> Pin<Box<dyn Future<Output = http::Response<hyper::Body>> + Send>> {
async fn _respond(status_code: u16) -> http::Response<hyper::Body> {
fn respond(&mut self) -> Pin<Box<dyn Future<Output = hyper::Response<hyper::Body>> + Send>> {
async fn _respond(status_code: u16) -> hyper::Response<hyper::Body> {
hyper::Response::builder()
.status(status_code)
.body(hyper::Body::empty())
@@ -23,17 +35,22 @@ impl Responder for StatusCode {
}
}
/// respond with a body that is the json encoding of data.
///
/// The status code will be `200` and the content-type will be
/// `application/json`.
pub fn json_encoded<T>(data: T) -> impl Responder
where
T: serde::Serialize,
{
JsonEncoded(serde_json::to_vec(&data).unwrap())
JsonEncoded(serde_json::to_string(&data).unwrap())
}
/// The `JsonEncoded` responder returned by [json_encoded()](fn.json_encoded.html)
#[derive(Debug)]
pub struct JsonEncoded(Vec<u8>);
pub struct JsonEncoded(String);
impl Responder for JsonEncoded {
fn respond(&mut self) -> Pin<Box<dyn Future<Output = http::Response<hyper::Body>> + Send>> {
async fn _respond(body: Vec<u8>) -> http::Response<hyper::Body> {
fn respond(&mut self) -> Pin<Box<dyn Future<Output = hyper::Response<hyper::Body>> + Send>> {
async fn _respond(body: String) -> hyper::Response<hyper::Body> {
hyper::Response::builder()
.status(200)
.header("Content-Type", "application/json")
@@ -44,14 +61,42 @@ impl Responder for JsonEncoded {
}
}
impl Responder for crate::FullResponse {
fn respond(&mut self) -> Pin<Box<dyn Future<Output = http::Response<hyper::Body>> + Send>> {
async fn _respond(resp: http::Response<hyper::Body>) -> http::Response<hyper::Body> {
/// respond with a body that is the url encoding of data.
///
/// The status code will be `200` and the content-type will be
/// `application/x-www-form-urlencoded`.
pub fn url_encoded<T>(data: T) -> impl Responder
where
T: serde::Serialize,
{
UrlEncoded(serde_urlencoded::to_string(&data).unwrap())
}
/// The `UrlEncoded` responder returned by [url_encoded()](fn.url_encoded.html)
#[derive(Debug)]
pub struct UrlEncoded(String);
impl Responder for UrlEncoded {
fn respond(&mut self) -> Pin<Box<dyn Future<Output = hyper::Response<hyper::Body>> + Send>> {
async fn _respond(body: String) -> hyper::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>
where
B: Clone + Into<hyper::Body> + Send + fmt::Debug,
{
fn respond(&mut self) -> Pin<Box<dyn Future<Output = hyper::Response<hyper::Body>> + Send>> {
async fn _respond(resp: hyper::Response<hyper::Body>) -> hyper::Response<hyper::Body> {
resp
}
// Turn &hyper::Response<Vec<u8>> into a hyper::Response<hyper::Body>
let mut builder = hyper::Response::builder();
builder
builder = builder
.status(self.status().clone())
.version(self.version().clone());
*builder.headers_mut().unwrap() = self.headers().clone();
@@ -61,20 +106,21 @@ impl Responder for crate::FullResponse {
}
}
// TODO: make a macro for this to avoid the vec![Box::new] dance.
/// Cycle through the provided list of responders.
pub fn cycle(responders: Vec<Box<dyn Responder>>) -> impl Responder {
if responders.is_empty() {
panic!("empty vector provided to cycle");
}
Cycle { idx: 0, responders }
}
/// The `Cycle` responder returned by [cycle()](fn.cycle.html)
#[derive(Debug)]
pub struct Cycle {
idx: usize,
responders: Vec<Box<dyn Responder>>,
}
impl Responder for Cycle {
fn respond(&mut self) -> Pin<Box<dyn Future<Output = http::Response<hyper::Body>> + Send>> {
fn respond(&mut self) -> Pin<Box<dyn Future<Output = hyper::Response<hyper::Body>> + Send>> {
let response = self.responders[self.idx].respond();
self.idx = (self.idx + 1) % self.responders.len();
response

View File

@@ -1,10 +1,14 @@
use crate::mappers::Matcher;
use crate::responders::Responder;
use crate::{FullRequest, Matcher};
use std::future::Future;
use std::net::SocketAddr;
use std::pin::Pin;
use std::sync::{Arc, Mutex};
// type alias for a request that has read a complete body into memory.
type FullRequest = hyper::Request<hyper::body::Bytes>;
/// The Server
pub struct Server {
trigger_shutdown: Option<futures::channel::oneshot::Sender<()>>,
join_handle: Option<std::thread::JoinHandle<()>>,
@@ -13,6 +17,10 @@ pub struct Server {
}
impl Server {
/// Start a server.
///
/// The server will run in the background. On Drop it will terminate and
/// assert it's expectations.
pub fn run() -> Self {
use futures::future::FutureExt;
use hyper::{
@@ -35,9 +43,8 @@ impl Server {
async move {
// read the full body into memory prior to handing it to mappers.
let (head, body) = req.into_parts();
use futures::TryStreamExt;
let full_body = body.try_concat().await?;
let req = hyper::Request::from_parts(head, full_body.to_vec());
let full_body = hyper::body::to_bytes(body).await?;
let req = hyper::Request::from_parts(head, full_body);
log::debug!("Received Request: {:?}", req);
let resp = on_req(state, req).await;
log::debug!("Sending Response: {:?}", resp);
@@ -53,7 +60,11 @@ impl Server {
let addr = server.local_addr();
let (trigger_shutdown, shutdown_received) = futures::channel::oneshot::channel();
let join_handle = std::thread::spawn(move || {
let mut runtime = tokio::runtime::current_thread::Runtime::new().unwrap();
let mut runtime = tokio::runtime::Builder::new()
.basic_scheduler()
.enable_all()
.build()
.unwrap();
runtime.block_on(async move {
futures::select! {
_ = server.fuse() => {},
@@ -70,15 +81,22 @@ impl Server {
}
}
/// Get the address the server is listening on.
pub fn addr(&self) -> SocketAddr {
self.addr
}
pub fn url<T>(&self, path_and_query: T) -> http::Uri
/// Get a fully formed url to the servers address.
///
/// If the server is listening on port 1234.
///
/// `server.url("/foo?q=1") == "http://localhost:1234/foo?q=1"`
pub fn url<T>(&self, path_and_query: T) -> hyper::Uri
where
http::uri::PathAndQuery: http::HttpTryFrom<T>,
http::uri::PathAndQuery: std::convert::TryFrom<T>,
<http::uri::PathAndQuery as std::convert::TryFrom<T>>::Error: Into<http::Error>,
{
http::Uri::builder()
hyper::Uri::builder()
.scheme("http")
.authority(format!("{}", &self.addr).as_str())
.path_and_query(path_and_query)
@@ -86,12 +104,20 @@ impl Server {
.unwrap()
}
/// Add a new expectation to the server.
pub fn expect(&self, expectation: Expectation) {
self.state.push_expectation(expectation);
}
/// Verify all registered expectations. Panic if any are not met, then clear
/// 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,
@@ -134,7 +160,7 @@ impl Drop for Server {
}
}
async fn on_req(state: ServerState, req: FullRequest) -> http::Response<hyper::Body> {
async fn on_req(state: ServerState, req: FullRequest) -> hyper::Response<hyper::Body> {
let response_future = {
let mut state = state.lock();
let mut iter = state.expected.iter_mut();
@@ -176,22 +202,29 @@ async fn on_req(state: ServerState, req: FullRequest) -> http::Response<hyper::B
if let Some(f) = response_future {
f.await
} else {
http::Response::builder()
.status(http::StatusCode::INTERNAL_SERVER_ERROR)
hyper::Response::builder()
.status(hyper::StatusCode::INTERNAL_SERVER_ERROR)
.body(hyper::Body::empty())
.unwrap()
}
}
/// How many requests should an expectation receive.
#[derive(Debug, Clone)]
pub enum Times {
/// Allow any number of requests.
AnyNumber,
/// Require that at least this many requests are received.
AtLeast(usize),
/// Require that no more than this many requests are received.
AtMost(usize),
/// Require that the number of requests received is within this range.
Between(std::ops::RangeInclusive<usize>),
/// Require that exactly this many requests are received.
Exactly(usize),
}
/// An expectation to be asserted by the server.
pub struct Expectation {
matcher: Box<dyn Matcher<FullRequest>>,
cardinality: Times,
@@ -200,6 +233,7 @@ pub struct Expectation {
}
impl Expectation {
/// What requests will this expectation match.
pub fn matching(matcher: impl Matcher<FullRequest> + 'static) -> ExpectationBuilder {
ExpectationBuilder {
matcher: Box::new(matcher),
@@ -208,12 +242,14 @@ impl Expectation {
}
}
/// Define expectations using a builder pattern.
pub struct ExpectationBuilder {
matcher: Box<dyn Matcher<FullRequest>>,
cardinality: Times,
}
impl ExpectationBuilder {
/// How many requests should this expectation receive.
pub fn times(self, cardinality: Times) -> ExpectationBuilder {
ExpectationBuilder {
cardinality,
@@ -221,6 +257,7 @@ impl ExpectationBuilder {
}
}
/// What should this expectation respond with.
pub fn respond_with(self, responder: impl Responder + 'static) -> Expectation {
Expectation {
matcher: self.matcher,
@@ -272,14 +309,14 @@ fn cardinality_error(
matcher: &dyn Matcher<FullRequest>,
cardinality: &Times,
hit_count: usize,
) -> Pin<Box<dyn Future<Output = http::Response<hyper::Body>> + Send + 'static>> {
) -> Pin<Box<dyn Future<Output = hyper::Response<hyper::Body>> + Send + 'static>> {
let body = hyper::Body::from(format!(
"Unexpected number of requests for matcher '{:?}'; received {}; expected {:?}",
matcher, hit_count, cardinality,
));
Box::pin(async move {
http::Response::builder()
.status(http::StatusCode::INTERNAL_SERVER_ERROR)
hyper::Response::builder()
.status(hyper::StatusCode::INTERNAL_SERVER_ERROR)
.body(body)
.unwrap()
})

View File

@@ -1,9 +1,10 @@
use httptest::{mappers::*, responders::*, Expectation, Times};
async fn read_response_body(resp: hyper::Response<hyper::Body>) -> hyper::Response<Vec<u8>> {
use futures::stream::TryStreamExt;
async fn read_response_body(
resp: hyper::Response<hyper::Body>,
) -> hyper::Response<hyper::body::Bytes> {
let (head, body) = resp.into_parts();
let body = body.try_concat().await.unwrap().to_vec();
let body = hyper::body::to_bytes(body).await.unwrap();
hyper::Response::from_parts(head, body)
}
@@ -84,6 +85,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 +105,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));
@@ -126,10 +133,7 @@ async fn test_cycle() {
request::path(eq("/foo"))
])
.times(Times::Exactly(4))
.respond_with(cycle(vec![
Box::new(status_code(200)),
Box::new(status_code(404)),
])),
.respond_with(cycle![status_code(200), status_code(404),]),
);
// Issue multiple GET /foo to the server and verify it alternates between 200 and 404 codes.
@@ -143,3 +147,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.
}