diff --git a/src/lib.rs b/src/lib.rs index b01c6ef..69e6cc1 100644 --- a/src/lib.rs +++ b/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:///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>` 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> 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>; -pub type FullResponse = hyper::Response>; -pub use mappers::Matcher; - -pub use server::Expectation; -pub use server::Server; -pub use server::Times; +pub use server::{Expectation, ExpectationBuilder, Server, Times}; diff --git a/src/mappers.rs b/src/mappers.rs index 4ba7565..28fa0af 100644 --- a/src/mappers.rs +++ b/src/mappers.rs @@ -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() -> impl Mapper { +pub fn any() -> Any { Any } #[derive(Debug)] @@ -49,10 +48,9 @@ impl Mapper for Any { } } -pub fn contains(value: T) -> impl Mapper +pub fn contains(value: T) -> Contains where T: AsRef<[u8]> + fmt::Debug + Send, - IN: AsRef<[u8]> + ?Sized, { Contains(value) } @@ -71,11 +69,7 @@ where } } -pub fn eq(value: T) -> impl Mapper -where - T: Borrow + fmt::Debug + Send, - IN: PartialEq + ?Sized, -{ +pub fn eq(value: T) -> Eq { Eq(value) } #[derive(Debug)] @@ -92,10 +86,7 @@ where } } -pub fn matches(value: &str) -> impl Mapper -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(inner: C) -> impl Mapper -where - C: Mapper, - IN: ?Sized, -{ - Not(inner, PhantomData) +pub fn not(inner: C) -> Not { + Not(inner) } -pub struct Not(C, PhantomData) -where - IN: ?Sized; -impl Mapper for Not +pub struct Not(C); +impl Mapper for Not where C: Mapper, IN: ?Sized, @@ -133,17 +118,16 @@ where !self.0.map(input) } } -impl fmt::Debug for Not +impl fmt::Debug for Not where - C: Mapper, - IN: ?Sized, + C: fmt::Debug, { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { write!(f, "Not({:?})", &self.0) } } -pub fn all_of(inner: Vec>>) -> impl Mapper +pub fn all_of(inner: Vec>>) -> AllOf where IN: fmt::Debug + ?Sized, { @@ -165,7 +149,7 @@ where } } -pub fn any_of(inner: Vec>>) -> impl Mapper +pub fn any_of(inner: Vec>>) -> AnyOf where IN: fmt::Debug + ?Sized, { @@ -186,9 +170,8 @@ where } } -pub fn uri_decoded(inner: C) -> impl Mapper +pub fn uri_decoded(inner: C) -> UriDecoded where - IN: AsRef<[u8]> + ?Sized, C: Mapper<[(String, String)]>, { UriDecoded(inner) @@ -210,9 +193,8 @@ where } } -pub fn json_decoded(inner: C) -> impl Mapper +pub fn json_decoded(inner: C) -> JsonDecoded where - IN: AsRef<[u8]> + ?Sized, C: Mapper, { JsonDecoded(inner) @@ -233,9 +215,8 @@ where } } -pub fn lowercase(inner: C) -> impl Mapper +pub fn lowercase(inner: C) -> Lowercase 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)); diff --git a/src/mappers/request.rs b/src/mappers/request.rs index e18effa..cc0cb9a 100644 --- a/src/mappers/request.rs +++ b/src/mappers/request.rs @@ -1,7 +1,6 @@ use super::Mapper; -use crate::FullRequest; -pub fn method(inner: C) -> impl Mapper +pub fn method(inner: C) -> Method where C: Mapper, { @@ -9,18 +8,18 @@ where } #[derive(Debug)] pub struct Method(C); -impl Mapper for Method +impl Mapper> for Method where C: Mapper, { type Out = C::Out; - fn map(&mut self, input: &FullRequest) -> C::Out { + fn map(&mut self, input: &hyper::Request) -> C::Out { self.0.map(input.method().as_str()) } } -pub fn path(inner: C) -> impl Mapper +pub fn path(inner: C) -> Path where C: Mapper, { @@ -28,18 +27,18 @@ where } #[derive(Debug)] pub struct Path(C); -impl Mapper for Path +impl Mapper> for Path where C: Mapper, { type Out = C::Out; - fn map(&mut self, input: &FullRequest) -> C::Out { + fn map(&mut self, input: &hyper::Request) -> C::Out { self.0.map(input.uri().path()) } } -pub fn query(inner: C) -> impl Mapper +pub fn query(inner: C) -> Query where C: Mapper, { @@ -47,18 +46,18 @@ where } #[derive(Debug)] pub struct Query(C); -impl Mapper for Query +impl Mapper> for Query where C: Mapper, { type Out = C::Out; - fn map(&mut self, input: &FullRequest) -> C::Out { + fn map(&mut self, input: &hyper::Request) -> C::Out { self.0.map(input.uri().query().unwrap_or("")) } } -pub fn headers(inner: C) -> impl Mapper +pub fn headers(inner: C) -> Headers where C: Mapper<[(Vec, Vec)]>, { @@ -66,13 +65,13 @@ where } #[derive(Debug)] pub struct Headers(C); -impl Mapper for Headers +impl Mapper> for Headers where C: Mapper<[(Vec, Vec)]>, { type Out = C::Out; - fn map(&mut self, input: &FullRequest) -> C::Out { + fn map(&mut self, input: &hyper::Request) -> C::Out { let headers: Vec<(Vec, Vec)> = input .headers() .iter() @@ -82,21 +81,18 @@ where } } -pub fn body(inner: C) -> impl Mapper -where - C: Mapper<[u8]>, -{ +pub fn body(inner: C) -> Body { Body(inner) } #[derive(Debug)] pub struct Body(C); -impl Mapper for Body +impl Mapper> for Body where - C: Mapper<[u8]>, + C: Mapper, { type Out = C::Out; - fn map(&mut self, input: &FullRequest) -> C::Out { + fn map(&mut self, input: &hyper::Request) -> 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)); } } diff --git a/src/mappers/response.rs b/src/mappers/response.rs index 2c45a00..dd68880 100644 --- a/src/mappers/response.rs +++ b/src/mappers/response.rs @@ -1,7 +1,6 @@ use super::Mapper; -use crate::FullResponse; -pub fn status_code(inner: C) -> impl Mapper +pub fn status_code(inner: C) -> StatusCode where C: Mapper, { @@ -9,18 +8,18 @@ where } #[derive(Debug)] pub struct StatusCode(C); -impl Mapper for StatusCode +impl Mapper> for StatusCode where C: Mapper, { type Out = C::Out; - fn map(&mut self, input: &FullResponse) -> C::Out { + fn map(&mut self, input: &hyper::Response) -> C::Out { self.0.map(&input.status().as_u16()) } } -pub fn headers(inner: C) -> impl Mapper +pub fn headers(inner: C) -> Headers where C: Mapper<[(Vec, Vec)]>, { @@ -28,13 +27,13 @@ where } #[derive(Debug)] pub struct Headers(C); -impl Mapper for Headers +impl Mapper> for Headers where C: Mapper<[(Vec, Vec)]>, { type Out = C::Out; - fn map(&mut self, input: &FullResponse) -> C::Out { + fn map(&mut self, input: &hyper::Response) -> C::Out { let headers: Vec<(Vec, Vec)> = input .headers() .iter() @@ -44,21 +43,18 @@ where } } -pub fn body(inner: C) -> impl Mapper -where - C: Mapper<[u8]>, -{ +pub fn body(inner: C) -> Body { Body(inner) } #[derive(Debug)] pub struct Body(C); -impl Mapper for Body +impl Mapper> for Body where - C: Mapper<[u8]>, + C: Mapper, { type Out = C::Out; - fn map(&mut self, input: &FullResponse) -> C::Out { + fn map(&mut self, input: &hyper::Response) -> 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)); } } diff --git a/src/responders.rs b/src/responders.rs index fd6a77e..9121384 100644 --- a/src/responders.rs +++ b/src/responders.rs @@ -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> + Send>>; } @@ -27,13 +29,13 @@ pub fn json_encoded(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); +pub struct JsonEncoded(String); impl Responder for JsonEncoded { fn respond(&mut self) -> Pin> + Send>> { - async fn _respond(body: Vec) -> http::Response { + async fn _respond(body: String) -> http::Response { hyper::Response::builder() .status(200) .header("Content-Type", "application/json") @@ -44,7 +46,10 @@ impl Responder for JsonEncoded { } } -impl Responder for crate::FullResponse { +impl Responder for hyper::Response +where + B: Clone + Into + Send + fmt::Debug, +{ fn respond(&mut self) -> Pin> + Send>> { async fn _respond(resp: http::Response) -> http::Response { 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>) -> impl Responder { if responders.is_empty() { panic!("empty vector provided to cycle"); diff --git a/src/server.rs b/src/server.rs index 5b747ee..5cec844 100644 --- a/src/server.rs +++ b/src/server.rs @@ -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>; + +/// The Server pub struct Server { trigger_shutdown: Option>, join_handle: Option>, @@ -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(&self, path_and_query: T) -> http::Uri where http::uri::PathAndQuery: http::HttpTryFrom, @@ -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), + /// Require that exactly this many requests are received. Exactly(usize), } +/// An expectation to be asserted by the server. pub struct Expectation { matcher: Box>, cardinality: Times, @@ -200,6 +224,7 @@ pub struct Expectation { } impl Expectation { + /// What requests will this expectation match. pub fn matching(matcher: impl Matcher + 'static) -> ExpectationBuilder { ExpectationBuilder { matcher: Box::new(matcher), @@ -208,12 +233,14 @@ impl Expectation { } } +/// Define expectations using a builder pattern. pub struct ExpectationBuilder { matcher: Box>, 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, diff --git a/tests/tests.rs b/tests/tests.rs index 0f707d8..bed2b34 100644 --- a/tests/tests.rs +++ b/tests/tests.rs @@ -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.