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_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};

View File

@ -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));

View File

@ -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));
}
}

View File

@ -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));
}
}

View File

@ -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");

View File

@ -1,10 +1,14 @@
use crate::mappers::Matcher;
use crate::responders::Responder;
use crate::{FullRequest, Matcher};
use std::future::Future;
use std::net::SocketAddr;
use std::pin::Pin;
use std::sync::{Arc, Mutex};
// type alias for a request that has read a complete body into memory.
type FullRequest = hyper::Request<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,

View File

@ -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.