Save work

This commit is contained in:
Glenn Griffin 2019-12-07 23:01:04 -08:00
parent e889b56378
commit 2206ddc520
7 changed files with 280 additions and 105 deletions

View File

@ -1,15 +1,198 @@
//! # httptest
//!
//! Provide convenient mechanism for testing http clients against a locally
//! running http server. This library consists of a number of components that
//! allow starting an http server and configuring it to expect to receive certain
//! requests and respond appropriately.
//!
//! ## 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 [Server::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 [server.verify_and_clear()](struct.Server.html#method.verify_and_clear)
//! to assert and clear the expectations while keeping the server running.
//!
//! [server.addr()](struct.Server.html#method.addr) will return the address the
//! server is listening on.
//!
//! [server.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 composoble 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<Vec<u8>>` 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 requests 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
//! [Expectation.times()](struct.Expectation.html#method.times) method.
//!
//! ## 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<Vec<u8>> or obviously 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)]
// hidden from docs here because it's re-rexported from the mappers module.
#[doc(hidden)]
#[macro_export] #[macro_export]
macro_rules! all_of { macro_rules! all_of {
($($x:expr),*) => ($crate::mappers::all_of($crate::vec_of_boxes![$($x),*])); ($($x:expr),*) => ($crate::mappers::all_of($crate::vec_of_boxes![$($x),*]));
($($x:expr,)*) => ($crate::all_of![$($x),*]); ($($x:expr,)*) => ($crate::all_of![$($x),*]);
} }
// hidden from docs here because it's re-rexported from the mappers module.
#[doc(hidden)]
#[macro_export] #[macro_export]
macro_rules! any_of { macro_rules! any_of {
($($x:expr),*) => ($crate::mappers::any_of($crate::vec_of_boxes![$($x),*])); ($($x:expr),*) => ($crate::mappers::any_of($crate::vec_of_boxes![$($x),*]));
($($x:expr,)*) => ($crate::any_of![$($x),*]); ($($x:expr,)*) => ($crate::any_of![$($x),*]);
} }
// hidden from docs here because it's re-rexported from the responders module.
#[doc(hidden)]
#[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_export]
macro_rules! vec_of_boxes { macro_rules! vec_of_boxes {
($($x:expr),*) => (std::vec![$(std::boxed::Box::new($x)),*]); ($($x:expr),*) => (std::vec![$(std::boxed::Box::new($x)),*]);
@ -18,12 +201,6 @@ macro_rules! vec_of_boxes {
pub mod mappers; pub mod mappers;
pub mod responders; pub mod responders;
pub mod server; mod server;
pub type FullRequest = hyper::Request<Vec<u8>>; pub use server::{Expectation, ExpectationBuilder, Server, Times};
pub type FullResponse = hyper::Response<Vec<u8>>;
pub use mappers::Matcher;
pub use server::Expectation;
pub use server::Server;
pub use server::Times;

View File

@ -1,6 +1,5 @@
use std::borrow::Borrow; use std::borrow::Borrow;
use std::fmt; use std::fmt;
use std::marker::PhantomData;
// import the any_of and all_of macros from crate root so they are accessible if // import the any_of and all_of macros from crate root so they are accessible if
// people glob import this module. // people glob import this module.
@ -36,7 +35,7 @@ where
} }
} }
pub fn any<IN>() -> impl Mapper<IN, Out = bool> { pub fn any() -> Any {
Any Any
} }
#[derive(Debug)] #[derive(Debug)]
@ -49,10 +48,9 @@ impl<IN> Mapper<IN> for Any {
} }
} }
pub fn contains<T, IN>(value: T) -> impl Mapper<IN, Out = bool> pub fn contains<T>(value: T) -> Contains<T>
where where
T: AsRef<[u8]> + fmt::Debug + Send, T: AsRef<[u8]> + fmt::Debug + Send,
IN: AsRef<[u8]> + ?Sized,
{ {
Contains(value) Contains(value)
} }
@ -71,11 +69,7 @@ where
} }
} }
pub fn eq<T, IN>(value: T) -> impl Mapper<IN, Out = bool> pub fn eq<T>(value: T) -> Eq<T> {
where
T: Borrow<IN> + fmt::Debug + Send,
IN: PartialEq + ?Sized,
{
Eq(value) Eq(value)
} }
#[derive(Debug)] #[derive(Debug)]
@ -92,10 +86,7 @@ where
} }
} }
pub fn matches<IN>(value: &str) -> impl Mapper<IN, Out = bool> pub fn matches(value: &str) -> Matches {
where
IN: AsRef<[u8]> + ?Sized,
{
let regex = regex::bytes::Regex::new(value).expect("failed to create regex"); let regex = regex::bytes::Regex::new(value).expect("failed to create regex");
Matches(regex) Matches(regex)
} }
@ -112,17 +103,11 @@ where
} }
} }
pub fn not<C, IN>(inner: C) -> impl Mapper<IN, Out = bool> pub fn not<C>(inner: C) -> Not<C> {
where Not(inner)
C: Mapper<IN, Out = bool>,
IN: ?Sized,
{
Not(inner, PhantomData)
} }
pub struct Not<C, IN>(C, PhantomData<fn(IN)>) pub struct Not<C>(C);
where impl<C, IN> Mapper<IN> for Not<C>
IN: ?Sized;
impl<C, IN> Mapper<IN> for Not<C, IN>
where where
C: Mapper<IN, Out = bool>, C: Mapper<IN, Out = bool>,
IN: ?Sized, IN: ?Sized,
@ -133,17 +118,16 @@ where
!self.0.map(input) !self.0.map(input)
} }
} }
impl<C, IN> fmt::Debug for Not<C, IN> impl<C> fmt::Debug for Not<C>
where where
C: Mapper<IN, Out = bool>, C: fmt::Debug,
IN: ?Sized,
{ {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "Not({:?})", &self.0) write!(f, "Not({:?})", &self.0)
} }
} }
pub fn all_of<IN>(inner: Vec<Box<dyn Mapper<IN, Out = bool>>>) -> impl Mapper<IN, Out = bool> pub fn all_of<IN>(inner: Vec<Box<dyn Mapper<IN, Out = bool>>>) -> AllOf<IN>
where where
IN: fmt::Debug + ?Sized, IN: fmt::Debug + ?Sized,
{ {
@ -165,7 +149,7 @@ where
} }
} }
pub fn any_of<IN>(inner: Vec<Box<dyn Mapper<IN, Out = bool>>>) -> impl Mapper<IN, Out = bool> pub fn any_of<IN>(inner: Vec<Box<dyn Mapper<IN, Out = bool>>>) -> AnyOf<IN>
where where
IN: fmt::Debug + ?Sized, IN: fmt::Debug + ?Sized,
{ {
@ -186,9 +170,8 @@ where
} }
} }
pub fn uri_decoded<IN, C>(inner: C) -> impl Mapper<IN, Out = C::Out> pub fn uri_decoded<C>(inner: C) -> UriDecoded<C>
where where
IN: AsRef<[u8]> + ?Sized,
C: Mapper<[(String, String)]>, C: Mapper<[(String, String)]>,
{ {
UriDecoded(inner) UriDecoded(inner)
@ -210,9 +193,8 @@ where
} }
} }
pub fn json_decoded<IN, C>(inner: C) -> impl Mapper<IN, Out = C::Out> pub fn json_decoded<C>(inner: C) -> JsonDecoded<C>
where where
IN: AsRef<[u8]> + ?Sized,
C: Mapper<serde_json::Value>, C: Mapper<serde_json::Value>,
{ {
JsonDecoded(inner) JsonDecoded(inner)
@ -233,9 +215,8 @@ where
} }
} }
pub fn lowercase<IN, C>(inner: C) -> impl Mapper<IN, Out = C::Out> pub fn lowercase<C>(inner: C) -> Lowercase<C>
where where
IN: AsRef<[u8]> + ?Sized,
C: Mapper<[u8]>, C: Mapper<[u8]>,
{ {
Lowercase(inner) Lowercase(inner)
@ -321,7 +302,7 @@ mod tests {
]; ];
let mut c = request::query(uri_decoded(eq(expected))); let mut c = request::query(uri_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(Vec::new()) .body("")
.unwrap(); .unwrap();
assert_eq!(true, c.map(&req)); assert_eq!(true, c.map(&req));

View File

@ -1,7 +1,6 @@
use super::Mapper; use super::Mapper;
use crate::FullRequest;
pub fn method<C>(inner: C) -> impl Mapper<FullRequest, Out = C::Out> pub fn method<C>(inner: C) -> Method<C>
where where
C: Mapper<str>, C: Mapper<str>,
{ {
@ -9,18 +8,18 @@ where
} }
#[derive(Debug)] #[derive(Debug)]
pub struct Method<C>(C); pub struct Method<C>(C);
impl<C> Mapper<FullRequest> for Method<C> impl<C, B> Mapper<hyper::Request<B>> for Method<C>
where where
C: Mapper<str>, C: Mapper<str>,
{ {
type Out = C::Out; type Out = C::Out;
fn map(&mut self, input: &FullRequest) -> C::Out { fn map(&mut self, input: &hyper::Request<B>) -> C::Out {
self.0.map(input.method().as_str()) self.0.map(input.method().as_str())
} }
} }
pub fn path<C>(inner: C) -> impl Mapper<FullRequest, Out = C::Out> pub fn path<C>(inner: C) -> Path<C>
where where
C: Mapper<str>, C: Mapper<str>,
{ {
@ -28,18 +27,18 @@ where
} }
#[derive(Debug)] #[derive(Debug)]
pub struct Path<C>(C); pub struct Path<C>(C);
impl<C> Mapper<FullRequest> for Path<C> impl<C, B> Mapper<hyper::Request<B>> for Path<C>
where where
C: Mapper<str>, C: Mapper<str>,
{ {
type Out = C::Out; type Out = C::Out;
fn map(&mut self, input: &FullRequest) -> C::Out { fn map(&mut self, input: &hyper::Request<B>) -> C::Out {
self.0.map(input.uri().path()) self.0.map(input.uri().path())
} }
} }
pub fn query<C>(inner: C) -> impl Mapper<FullRequest, Out = C::Out> pub fn query<C>(inner: C) -> Query<C>
where where
C: Mapper<str>, C: Mapper<str>,
{ {
@ -47,18 +46,18 @@ where
} }
#[derive(Debug)] #[derive(Debug)]
pub struct Query<C>(C); pub struct Query<C>(C);
impl<C> Mapper<FullRequest> for Query<C> impl<C, B> Mapper<hyper::Request<B>> for Query<C>
where where
C: Mapper<str>, C: Mapper<str>,
{ {
type Out = C::Out; type Out = C::Out;
fn map(&mut self, input: &FullRequest) -> C::Out { fn map(&mut self, input: &hyper::Request<B>) -> C::Out {
self.0.map(input.uri().query().unwrap_or("")) self.0.map(input.uri().query().unwrap_or(""))
} }
} }
pub fn headers<C>(inner: C) -> impl Mapper<FullRequest, Out = C::Out> pub fn headers<C>(inner: C) -> Headers<C>
where where
C: Mapper<[(Vec<u8>, Vec<u8>)]>, C: Mapper<[(Vec<u8>, Vec<u8>)]>,
{ {
@ -66,13 +65,13 @@ where
} }
#[derive(Debug)] #[derive(Debug)]
pub struct Headers<C>(C); pub struct Headers<C>(C);
impl<C> Mapper<FullRequest> for Headers<C> impl<C, B> Mapper<hyper::Request<B>> for Headers<C>
where where
C: Mapper<[(Vec<u8>, Vec<u8>)]>, C: Mapper<[(Vec<u8>, Vec<u8>)]>,
{ {
type Out = C::Out; type Out = C::Out;
fn map(&mut self, input: &FullRequest) -> C::Out { fn map(&mut self, input: &hyper::Request<B>) -> C::Out {
let headers: Vec<(Vec<u8>, Vec<u8>)> = input let headers: Vec<(Vec<u8>, Vec<u8>)> = input
.headers() .headers()
.iter() .iter()
@ -82,21 +81,18 @@ where
} }
} }
pub fn body<C>(inner: C) -> impl Mapper<FullRequest, Out = C::Out> pub fn body<C>(inner: C) -> Body<C> {
where
C: Mapper<[u8]>,
{
Body(inner) Body(inner)
} }
#[derive(Debug)] #[derive(Debug)]
pub struct Body<C>(C); pub struct Body<C>(C);
impl<C> Mapper<FullRequest> for Body<C> impl<C, B> Mapper<hyper::Request<B>> for Body<C>
where where
C: Mapper<[u8]>, C: Mapper<B>,
{ {
type Out = C::Out; type Out = C::Out;
fn map(&mut self, input: &FullRequest) -> C::Out { fn map(&mut self, input: &hyper::Request<B>) -> C::Out {
self.0.map(input.body()) self.0.map(input.body())
} }
} }
@ -109,12 +105,12 @@ mod tests {
#[test] #[test]
fn test_path() { fn test_path() {
let req = hyper::Request::get("https://example.com/foo") let req = hyper::Request::get("https://example.com/foo")
.body(Vec::new()) .body("")
.unwrap(); .unwrap();
assert!(path(eq("/foo")).map(&req)); assert!(path(eq("/foo")).map(&req));
let req = hyper::Request::get("https://example.com/foobar") let req = hyper::Request::get("https://example.com/foobar")
.body(Vec::new()) .body("")
.unwrap(); .unwrap();
assert!(path(eq("/foobar")).map(&req)) assert!(path(eq("/foobar")).map(&req))
} }
@ -122,11 +118,11 @@ mod tests {
#[test] #[test]
fn test_query() { fn test_query() {
let req = hyper::Request::get("https://example.com/path?foo=bar&baz=bat") let req = hyper::Request::get("https://example.com/path?foo=bar&baz=bat")
.body(Vec::new()) .body("")
.unwrap(); .unwrap();
assert!(query(eq("foo=bar&baz=bat")).map(&req)); assert!(query(eq("foo=bar&baz=bat")).map(&req));
let req = hyper::Request::get("https://example.com/path?search=1") let req = hyper::Request::get("https://example.com/path?search=1")
.body(Vec::new()) .body("")
.unwrap(); .unwrap();
assert!(query(eq("search=1")).map(&req)); assert!(query(eq("search=1")).map(&req));
} }
@ -134,11 +130,11 @@ mod tests {
#[test] #[test]
fn test_method() { fn test_method() {
let req = hyper::Request::get("https://example.com/foo") let req = hyper::Request::get("https://example.com/foo")
.body(Vec::new()) .body("")
.unwrap(); .unwrap();
assert!(method(eq("GET")).map(&req)); assert!(method(eq("GET")).map(&req));
let req = hyper::Request::post("https://example.com/foobar") let req = hyper::Request::post("https://example.com/foobar")
.body(Vec::new()) .body("")
.unwrap(); .unwrap();
assert!(method(eq("POST")).map(&req)); assert!(method(eq("POST")).map(&req));
} }
@ -150,7 +146,7 @@ mod tests {
(Vec::from("content-length"), Vec::from("101")), (Vec::from("content-length"), Vec::from("101")),
]; ];
let mut req = hyper::Request::get("https://example.com/path?key%201=value%201&key2") let mut req = hyper::Request::get("https://example.com/path?key%201=value%201&key2")
.body(Vec::new()) .body("")
.unwrap(); .unwrap();
req.headers_mut().extend(vec![ req.headers_mut().extend(vec![
( (
@ -168,10 +164,9 @@ mod tests {
#[test] #[test]
fn test_body() { fn test_body() {
use bstr::{ByteVec, B};
let req = hyper::Request::get("https://example.com/foo") let req = hyper::Request::get("https://example.com/foo")
.body(Vec::from_slice("my request body")) .body("my request body")
.unwrap(); .unwrap();
assert!(body(eq(B("my request body"))).map(&req)); assert!(body(eq("my request body")).map(&req));
} }
} }

View File

@ -1,7 +1,6 @@
use super::Mapper; use super::Mapper;
use crate::FullResponse;
pub fn status_code<C>(inner: C) -> impl Mapper<FullResponse, Out = C::Out> pub fn status_code<C>(inner: C) -> StatusCode<C>
where where
C: Mapper<u16>, C: Mapper<u16>,
{ {
@ -9,18 +8,18 @@ where
} }
#[derive(Debug)] #[derive(Debug)]
pub struct StatusCode<C>(C); pub struct StatusCode<C>(C);
impl<C> Mapper<FullResponse> for StatusCode<C> impl<C, B> Mapper<hyper::Response<B>> for StatusCode<C>
where where
C: Mapper<u16>, C: Mapper<u16>,
{ {
type Out = C::Out; type Out = C::Out;
fn map(&mut self, input: &FullResponse) -> C::Out { fn map(&mut self, input: &hyper::Response<B>) -> C::Out {
self.0.map(&input.status().as_u16()) self.0.map(&input.status().as_u16())
} }
} }
pub fn headers<C>(inner: C) -> impl Mapper<FullResponse, Out = C::Out> pub fn headers<C>(inner: C) -> Headers<C>
where where
C: Mapper<[(Vec<u8>, Vec<u8>)]>, C: Mapper<[(Vec<u8>, Vec<u8>)]>,
{ {
@ -28,13 +27,13 @@ where
} }
#[derive(Debug)] #[derive(Debug)]
pub struct Headers<C>(C); pub struct Headers<C>(C);
impl<C> Mapper<FullResponse> for Headers<C> impl<C, B> Mapper<hyper::Response<B>> for Headers<C>
where where
C: Mapper<[(Vec<u8>, Vec<u8>)]>, C: Mapper<[(Vec<u8>, Vec<u8>)]>,
{ {
type Out = C::Out; type Out = C::Out;
fn map(&mut self, input: &FullResponse) -> C::Out { fn map(&mut self, input: &hyper::Response<B>) -> C::Out {
let headers: Vec<(Vec<u8>, Vec<u8>)> = input let headers: Vec<(Vec<u8>, Vec<u8>)> = input
.headers() .headers()
.iter() .iter()
@ -44,21 +43,18 @@ where
} }
} }
pub fn body<C>(inner: C) -> impl Mapper<FullResponse, Out = C::Out> pub fn body<C>(inner: C) -> Body<C> {
where
C: Mapper<[u8]>,
{
Body(inner) Body(inner)
} }
#[derive(Debug)] #[derive(Debug)]
pub struct Body<C>(C); pub struct Body<C>(C);
impl<C> Mapper<FullResponse> for Body<C> impl<C, B> Mapper<hyper::Response<B>> for Body<C>
where where
C: Mapper<[u8]>, C: Mapper<B>,
{ {
type Out = C::Out; type Out = C::Out;
fn map(&mut self, input: &FullResponse) -> C::Out { fn map(&mut self, input: &hyper::Response<B>) -> C::Out {
self.0.map(input.body()) self.0.map(input.body())
} }
} }
@ -72,13 +68,13 @@ mod tests {
fn test_status_code() { fn test_status_code() {
let resp = hyper::Response::builder() let resp = hyper::Response::builder()
.status(hyper::StatusCode::NOT_FOUND) .status(hyper::StatusCode::NOT_FOUND)
.body(Vec::new()) .body("")
.unwrap(); .unwrap();
assert!(status_code(eq(404)).map(&resp)); assert!(status_code(eq(404)).map(&resp));
let resp = hyper::Response::builder() let resp = hyper::Response::builder()
.status(hyper::StatusCode::OK) .status(hyper::StatusCode::OK)
.body(Vec::new()) .body("")
.unwrap(); .unwrap();
assert!(status_code(eq(200)).map(&resp)); assert!(status_code(eq(200)).map(&resp));
} }
@ -92,7 +88,7 @@ mod tests {
let resp = hyper::Response::builder() let resp = hyper::Response::builder()
.header("host", "example.com") .header("host", "example.com")
.header("content-length", 101) .header("content-length", 101)
.body(Vec::new()) .body("")
.unwrap(); .unwrap();
assert!(headers(eq(expected)).map(&resp)); assert!(headers(eq(expected)).map(&resp));
@ -100,10 +96,7 @@ mod tests {
#[test] #[test]
fn test_body() { fn test_body() {
use bstr::{ByteVec, B}; let resp = hyper::Response::builder().body("my request body").unwrap();
let resp = hyper::Response::builder() assert!(body(eq("my request body")).map(&resp));
.body(Vec::from_slice("my request body"))
.unwrap();
assert!(body(eq(B("my request body"))).map(&resp));
} }
} }

View File

@ -2,6 +2,8 @@ use std::fmt;
use std::future::Future; use std::future::Future;
use std::pin::Pin; use std::pin::Pin;
pub use crate::cycle;
pub trait Responder: Send + fmt::Debug { pub trait Responder: Send + fmt::Debug {
fn respond(&mut self) -> Pin<Box<dyn Future<Output = http::Response<hyper::Body>> + Send>>; fn respond(&mut self) -> Pin<Box<dyn Future<Output = http::Response<hyper::Body>> + Send>>;
} }
@ -27,13 +29,13 @@ pub fn json_encoded<T>(data: T) -> impl Responder
where where
T: serde::Serialize, T: serde::Serialize,
{ {
JsonEncoded(serde_json::to_vec(&data).unwrap()) JsonEncoded(serde_json::to_string(&data).unwrap())
} }
#[derive(Debug)] #[derive(Debug)]
pub struct JsonEncoded(Vec<u8>); pub struct JsonEncoded(String);
impl Responder for JsonEncoded { impl Responder for JsonEncoded {
fn respond(&mut self) -> Pin<Box<dyn Future<Output = http::Response<hyper::Body>> + Send>> { fn respond(&mut self) -> Pin<Box<dyn Future<Output = http::Response<hyper::Body>> + Send>> {
async fn _respond(body: Vec<u8>) -> http::Response<hyper::Body> { async fn _respond(body: String) -> http::Response<hyper::Body> {
hyper::Response::builder() hyper::Response::builder()
.status(200) .status(200)
.header("Content-Type", "application/json") .header("Content-Type", "application/json")
@ -44,7 +46,10 @@ impl Responder for JsonEncoded {
} }
} }
impl Responder for crate::FullResponse { 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 = http::Response<hyper::Body>> + Send>> { 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> { async fn _respond(resp: http::Response<hyper::Body>) -> http::Response<hyper::Body> {
resp resp
@ -61,7 +66,6 @@ impl Responder for crate::FullResponse {
} }
} }
// TODO: make a macro for this to avoid the vec![Box::new] dance.
pub fn cycle(responders: Vec<Box<dyn Responder>>) -> impl Responder { pub fn cycle(responders: Vec<Box<dyn Responder>>) -> impl Responder {
if responders.is_empty() { if responders.is_empty() {
panic!("empty vector provided to cycle"); panic!("empty vector provided to cycle");

View File

@ -1,10 +1,14 @@
use crate::mappers::Matcher;
use crate::responders::Responder; use crate::responders::Responder;
use crate::{FullRequest, Matcher};
use std::future::Future; use std::future::Future;
use std::net::SocketAddr; use std::net::SocketAddr;
use std::pin::Pin; use std::pin::Pin;
use std::sync::{Arc, Mutex}; use std::sync::{Arc, Mutex};
// type alias for a request that has read a complete body into memory.
type FullRequest = hyper::Request<Vec<u8>>;
/// The Server
pub struct Server { pub struct Server {
trigger_shutdown: Option<futures::channel::oneshot::Sender<()>>, trigger_shutdown: Option<futures::channel::oneshot::Sender<()>>,
join_handle: Option<std::thread::JoinHandle<()>>, join_handle: Option<std::thread::JoinHandle<()>>,
@ -13,6 +17,10 @@ pub struct Server {
} }
impl 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 { pub fn run() -> Self {
use futures::future::FutureExt; use futures::future::FutureExt;
use hyper::{ use hyper::{
@ -70,10 +78,16 @@ impl Server {
} }
} }
/// Get the address the server is listening on.
pub fn addr(&self) -> SocketAddr { pub fn addr(&self) -> SocketAddr {
self.addr self.addr
} }
/// 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) -> http::Uri pub fn url<T>(&self, path_and_query: T) -> http::Uri
where where
http::uri::PathAndQuery: http::HttpTryFrom<T>, http::uri::PathAndQuery: http::HttpTryFrom<T>,
@ -86,10 +100,13 @@ impl Server {
.unwrap() .unwrap()
} }
/// Add a new expectation to the server.
pub fn expect(&self, expectation: Expectation) { pub fn expect(&self, expectation: Expectation) {
self.state.push_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) { pub fn verify_and_clear(&mut self) {
let mut state = self.state.lock(); let mut state = self.state.lock();
for expectation in state.expected.iter() { for expectation in state.expected.iter() {
@ -183,15 +200,22 @@ async fn on_req(state: ServerState, req: FullRequest) -> http::Response<hyper::B
} }
} }
/// How many requests should an expectation receive.
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub enum Times { pub enum Times {
/// Allow any number of requests.
AnyNumber, AnyNumber,
/// Require that at least this many requests are received.
AtLeast(usize), AtLeast(usize),
/// Require that no more than this many requests are received.
AtMost(usize), AtMost(usize),
/// Require that the number of requests received is within this range.
Between(std::ops::RangeInclusive<usize>), Between(std::ops::RangeInclusive<usize>),
/// Require that exactly this many requests are received.
Exactly(usize), Exactly(usize),
} }
/// An expectation to be asserted by the server.
pub struct Expectation { pub struct Expectation {
matcher: Box<dyn Matcher<FullRequest>>, matcher: Box<dyn Matcher<FullRequest>>,
cardinality: Times, cardinality: Times,
@ -200,6 +224,7 @@ pub struct Expectation {
} }
impl Expectation { impl Expectation {
/// What requests will this expectation match.
pub fn matching(matcher: impl Matcher<FullRequest> + 'static) -> ExpectationBuilder { pub fn matching(matcher: impl Matcher<FullRequest> + 'static) -> ExpectationBuilder {
ExpectationBuilder { ExpectationBuilder {
matcher: Box::new(matcher), matcher: Box::new(matcher),
@ -208,12 +233,14 @@ impl Expectation {
} }
} }
/// Define expectations using a builder pattern.
pub struct ExpectationBuilder { pub struct ExpectationBuilder {
matcher: Box<dyn Matcher<FullRequest>>, matcher: Box<dyn Matcher<FullRequest>>,
cardinality: Times, cardinality: Times,
} }
impl ExpectationBuilder { impl ExpectationBuilder {
/// How many requests should this expectation receive.
pub fn times(self, cardinality: Times) -> ExpectationBuilder { pub fn times(self, cardinality: Times) -> ExpectationBuilder {
ExpectationBuilder { ExpectationBuilder {
cardinality, cardinality,
@ -221,6 +248,7 @@ impl ExpectationBuilder {
} }
} }
/// What should this expectation respond with.
pub fn respond_with(self, responder: impl Responder + 'static) -> Expectation { pub fn respond_with(self, responder: impl Responder + 'static) -> Expectation {
Expectation { Expectation {
matcher: self.matcher, matcher: self.matcher,

View File

@ -126,10 +126,7 @@ async fn test_cycle() {
request::path(eq("/foo")) request::path(eq("/foo"))
]) ])
.times(Times::Exactly(4)) .times(Times::Exactly(4))
.respond_with(cycle(vec![ .respond_with(cycle![status_code(200), status_code(404),]),
Box::new(status_code(200)),
Box::new(status_code(404)),
])),
); );
// Issue multiple GET /foo to the server and verify it alternates between 200 and 404 codes. // Issue multiple GET /foo to the server and verify it alternates between 200 and 404 codes.