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_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;
|
|
||||||
|
|||||||
@ -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));
|
||||||
|
|||||||
@ -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));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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");
|
||||||
|
|||||||
@ -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,
|
||||||
|
|||||||
@ -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.
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user