Save work
This commit is contained in:
parent
e889b56378
commit
2206ddc520
193
src/lib.rs
193
src/lib.rs
@ -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_rules! all_of {
|
||||
($($x:expr),*) => ($crate::mappers::all_of($crate::vec_of_boxes![$($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_rules! any_of {
|
||||
($($x:expr),*) => ($crate::mappers::any_of($crate::vec_of_boxes![$($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_rules! vec_of_boxes {
|
||||
($($x:expr),*) => (std::vec![$(std::boxed::Box::new($x)),*]);
|
||||
@ -18,12 +201,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};
|
||||
|
||||
@ -1,6 +1,5 @@
|
||||
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.
|
||||
@ -36,7 +35,7 @@ where
|
||||
}
|
||||
}
|
||||
|
||||
pub fn any<IN>() -> impl Mapper<IN, Out = bool> {
|
||||
pub fn any() -> Any {
|
||||
Any
|
||||
}
|
||||
#[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
|
||||
T: AsRef<[u8]> + fmt::Debug + Send,
|
||||
IN: AsRef<[u8]> + ?Sized,
|
||||
{
|
||||
Contains(value)
|
||||
}
|
||||
@ -71,11 +69,7 @@ where
|
||||
}
|
||||
}
|
||||
|
||||
pub fn eq<T, IN>(value: T) -> impl Mapper<IN, Out = bool>
|
||||
where
|
||||
T: Borrow<IN> + fmt::Debug + Send,
|
||||
IN: PartialEq + ?Sized,
|
||||
{
|
||||
pub fn eq<T>(value: T) -> Eq<T> {
|
||||
Eq(value)
|
||||
}
|
||||
#[derive(Debug)]
|
||||
@ -92,10 +86,7 @@ where
|
||||
}
|
||||
}
|
||||
|
||||
pub fn matches<IN>(value: &str) -> impl Mapper<IN, Out = bool>
|
||||
where
|
||||
IN: AsRef<[u8]> + ?Sized,
|
||||
{
|
||||
pub fn matches(value: &str) -> Matches {
|
||||
let regex = regex::bytes::Regex::new(value).expect("failed to create regex");
|
||||
Matches(regex)
|
||||
}
|
||||
@ -112,17 +103,11 @@ where
|
||||
}
|
||||
}
|
||||
|
||||
pub fn not<C, IN>(inner: C) -> impl Mapper<IN, Out = bool>
|
||||
where
|
||||
C: Mapper<IN, Out = bool>,
|
||||
IN: ?Sized,
|
||||
{
|
||||
Not(inner, PhantomData)
|
||||
pub fn not<C>(inner: C) -> Not<C> {
|
||||
Not(inner)
|
||||
}
|
||||
pub struct Not<C, IN>(C, PhantomData<fn(IN)>)
|
||||
where
|
||||
IN: ?Sized;
|
||||
impl<C, IN> Mapper<IN> for Not<C, IN>
|
||||
pub struct Not<C>(C);
|
||||
impl<C, IN> Mapper<IN> for Not<C>
|
||||
where
|
||||
C: Mapper<IN, Out = bool>,
|
||||
IN: ?Sized,
|
||||
@ -133,17 +118,16 @@ where
|
||||
!self.0.map(input)
|
||||
}
|
||||
}
|
||||
impl<C, IN> fmt::Debug for Not<C, IN>
|
||||
impl<C> fmt::Debug for Not<C>
|
||||
where
|
||||
C: Mapper<IN, Out = bool>,
|
||||
IN: ?Sized,
|
||||
C: 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>
|
||||
pub fn all_of<IN>(inner: Vec<Box<dyn Mapper<IN, Out = bool>>>) -> AllOf<IN>
|
||||
where
|
||||
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
|
||||
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
|
||||
IN: AsRef<[u8]> + ?Sized,
|
||||
C: Mapper<[(String, String)]>,
|
||||
{
|
||||
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
|
||||
IN: AsRef<[u8]> + ?Sized,
|
||||
C: Mapper<serde_json::Value>,
|
||||
{
|
||||
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
|
||||
IN: AsRef<[u8]> + ?Sized,
|
||||
C: Mapper<[u8]>,
|
||||
{
|
||||
Lowercase(inner)
|
||||
@ -321,7 +302,7 @@ mod tests {
|
||||
];
|
||||
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())
|
||||
.body("")
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(true, c.map(&req));
|
||||
|
||||
@ -1,7 +1,6 @@
|
||||
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
|
||||
C: Mapper<str>,
|
||||
{
|
||||
@ -9,18 +8,18 @@ where
|
||||
}
|
||||
#[derive(Debug)]
|
||||
pub struct Method<C>(C);
|
||||
impl<C> Mapper<FullRequest> for Method<C>
|
||||
impl<C, B> Mapper<hyper::Request<B>> for Method<C>
|
||||
where
|
||||
C: Mapper<str>,
|
||||
{
|
||||
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())
|
||||
}
|
||||
}
|
||||
|
||||
pub fn path<C>(inner: C) -> impl Mapper<FullRequest, Out = C::Out>
|
||||
pub fn path<C>(inner: C) -> Path<C>
|
||||
where
|
||||
C: Mapper<str>,
|
||||
{
|
||||
@ -28,18 +27,18 @@ where
|
||||
}
|
||||
#[derive(Debug)]
|
||||
pub struct Path<C>(C);
|
||||
impl<C> Mapper<FullRequest> for Path<C>
|
||||
impl<C, B> Mapper<hyper::Request<B>> for Path<C>
|
||||
where
|
||||
C: Mapper<str>,
|
||||
{
|
||||
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())
|
||||
}
|
||||
}
|
||||
|
||||
pub fn query<C>(inner: C) -> impl Mapper<FullRequest, Out = C::Out>
|
||||
pub fn query<C>(inner: C) -> Query<C>
|
||||
where
|
||||
C: Mapper<str>,
|
||||
{
|
||||
@ -47,18 +46,18 @@ where
|
||||
}
|
||||
#[derive(Debug)]
|
||||
pub struct Query<C>(C);
|
||||
impl<C> Mapper<FullRequest> for Query<C>
|
||||
impl<C, B> Mapper<hyper::Request<B>> for Query<C>
|
||||
where
|
||||
C: Mapper<str>,
|
||||
{
|
||||
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(""))
|
||||
}
|
||||
}
|
||||
|
||||
pub fn headers<C>(inner: C) -> impl Mapper<FullRequest, Out = C::Out>
|
||||
pub fn headers<C>(inner: C) -> Headers<C>
|
||||
where
|
||||
C: Mapper<[(Vec<u8>, Vec<u8>)]>,
|
||||
{
|
||||
@ -66,13 +65,13 @@ where
|
||||
}
|
||||
#[derive(Debug)]
|
||||
pub struct Headers<C>(C);
|
||||
impl<C> Mapper<FullRequest> for Headers<C>
|
||||
impl<C, B> Mapper<hyper::Request<B>> for Headers<C>
|
||||
where
|
||||
C: Mapper<[(Vec<u8>, Vec<u8>)]>,
|
||||
{
|
||||
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
|
||||
.headers()
|
||||
.iter()
|
||||
@ -82,21 +81,18 @@ where
|
||||
}
|
||||
}
|
||||
|
||||
pub fn body<C>(inner: C) -> impl Mapper<FullRequest, Out = C::Out>
|
||||
where
|
||||
C: Mapper<[u8]>,
|
||||
{
|
||||
pub fn body<C>(inner: C) -> Body<C> {
|
||||
Body(inner)
|
||||
}
|
||||
#[derive(Debug)]
|
||||
pub struct Body<C>(C);
|
||||
impl<C> Mapper<FullRequest> for Body<C>
|
||||
impl<C, B> Mapper<hyper::Request<B>> for Body<C>
|
||||
where
|
||||
C: Mapper<[u8]>,
|
||||
C: Mapper<B>,
|
||||
{
|
||||
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())
|
||||
}
|
||||
}
|
||||
@ -109,12 +105,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 +118,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 +130,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 +146,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 +164,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));
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,7 +1,6 @@
|
||||
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
|
||||
C: Mapper<u16>,
|
||||
{
|
||||
@ -9,18 +8,18 @@ where
|
||||
}
|
||||
#[derive(Debug)]
|
||||
pub struct StatusCode<C>(C);
|
||||
impl<C> Mapper<FullResponse> for StatusCode<C>
|
||||
impl<C, B> Mapper<hyper::Response<B>> for StatusCode<C>
|
||||
where
|
||||
C: Mapper<u16>,
|
||||
{
|
||||
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())
|
||||
}
|
||||
}
|
||||
|
||||
pub fn headers<C>(inner: C) -> impl Mapper<FullResponse, Out = C::Out>
|
||||
pub fn headers<C>(inner: C) -> Headers<C>
|
||||
where
|
||||
C: Mapper<[(Vec<u8>, Vec<u8>)]>,
|
||||
{
|
||||
@ -28,13 +27,13 @@ where
|
||||
}
|
||||
#[derive(Debug)]
|
||||
pub struct Headers<C>(C);
|
||||
impl<C> Mapper<FullResponse> for Headers<C>
|
||||
impl<C, B> Mapper<hyper::Response<B>> for Headers<C>
|
||||
where
|
||||
C: Mapper<[(Vec<u8>, Vec<u8>)]>,
|
||||
{
|
||||
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
|
||||
.headers()
|
||||
.iter()
|
||||
@ -44,21 +43,18 @@ where
|
||||
}
|
||||
}
|
||||
|
||||
pub fn body<C>(inner: C) -> impl Mapper<FullResponse, Out = C::Out>
|
||||
where
|
||||
C: Mapper<[u8]>,
|
||||
{
|
||||
pub fn body<C>(inner: C) -> Body<C> {
|
||||
Body(inner)
|
||||
}
|
||||
#[derive(Debug)]
|
||||
pub struct Body<C>(C);
|
||||
impl<C> Mapper<FullResponse> for Body<C>
|
||||
impl<C, B> Mapper<hyper::Response<B>> for Body<C>
|
||||
where
|
||||
C: Mapper<[u8]>,
|
||||
C: Mapper<B>,
|
||||
{
|
||||
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())
|
||||
}
|
||||
}
|
||||
@ -72,13 +68,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 +88,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 +96,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));
|
||||
}
|
||||
}
|
||||
|
||||
@ -2,6 +2,8 @@ use std::fmt;
|
||||
use std::future::Future;
|
||||
use std::pin::Pin;
|
||||
|
||||
pub use crate::cycle;
|
||||
|
||||
pub trait Responder: Send + fmt::Debug {
|
||||
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
|
||||
T: serde::Serialize,
|
||||
{
|
||||
JsonEncoded(serde_json::to_vec(&data).unwrap())
|
||||
JsonEncoded(serde_json::to_string(&data).unwrap())
|
||||
}
|
||||
#[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> {
|
||||
async fn _respond(body: String) -> http::Response<hyper::Body> {
|
||||
hyper::Response::builder()
|
||||
.status(200)
|
||||
.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>> {
|
||||
async fn _respond(resp: http::Response<hyper::Body>) -> http::Response<hyper::Body> {
|
||||
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 {
|
||||
if responders.is_empty() {
|
||||
panic!("empty vector provided to cycle");
|
||||
|
||||
@ -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<Vec<u8>>;
|
||||
|
||||
/// 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::{
|
||||
@ -70,10 +78,16 @@ impl Server {
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the address the server is listening on.
|
||||
pub fn addr(&self) -> SocketAddr {
|
||||
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
|
||||
where
|
||||
http::uri::PathAndQuery: http::HttpTryFrom<T>,
|
||||
@ -86,10 +100,13 @@ 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();
|
||||
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)]
|
||||
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 +224,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 +233,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 +248,7 @@ impl ExpectationBuilder {
|
||||
}
|
||||
}
|
||||
|
||||
/// What should this expectation respond with.
|
||||
pub fn respond_with(self, responder: impl Responder + 'static) -> Expectation {
|
||||
Expectation {
|
||||
matcher: self.matcher,
|
||||
|
||||
@ -126,10 +126,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.
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user