From 3e2d4662eb838ac2e702490e24d17d997c2b5236 Mon Sep 17 00:00:00 2001 From: Florin Lipan Date: Tue, 20 Feb 2024 19:42:45 +0100 Subject: [PATCH] Allow configuring the mock server (host, port, assert_on_drop) --- Cargo.toml | 2 +- src/lib.rs | 20 +++++++- src/mock.rs | 17 ++++++- src/server.rs | 118 ++++++++++++++++++++++++++++++++++++++------- src/server_pool.rs | 4 +- tests/lib.rs | 105 +++++++++++++++++++++++++++++++++++++++- 6 files changed, 241 insertions(+), 25 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index fddba44..70e9598 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -19,7 +19,7 @@ appveyor = { repository = "lipanski/mockito", branch = "master", service = "gith [dependencies] assert-json-diff = "2.0" -colored = { version = "2.0", optional = true } +colored = { version = "~2.0", optional = true } futures = "0.3" hyper = { version = "0.14", features = ["http1", "http2", "server", "stream"] } log = "0.4" diff --git a/src/lib.rs b/src/lib.rs index efc0912..4b999ea 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -150,6 +150,7 @@ //! In order to write async tests, you'll need to use the `_async` methods: //! //! - `Server::new_async` +//! - `Server::new_with_opts_async` //! - `Mock::create_async` //! - `Mock::assert_async` //! - `Mock::matched_async` @@ -162,6 +163,23 @@ //! This happens because a function attempted to block the current thread while the thread is being used to drive asynchronous tasks. //! ``` //! +//! # Configuring the server +//! +//! When calling `Server::new()`, a mock server with default options is returned from the server +//! pool. This should suffice for most use cases. +//! +//! If you'd like to bypass the server pool or configure the server in a different +//! way, you can use `Server::new_with_opts`. The following **options** are available: +//! +//! - `host`: allows setting the host (defaults to `127.0.0.1`) +//! - `port`: allows setting the port (defaults to a randomly assigned free port) +//! - `assert_on_drop`: automatically call `Mock::assert()` before dropping a mock (defaults to `false`) +//! +//! ``` +//! let opts = mockito::ServerOpts { assert_on_drop: true, ..Default::default() }; +//! let server = mockito::Server::new_with_opts(opts); +//! ``` +//! //! # Matchers //! //! Mockito can match your request by method, path, query, headers or body. @@ -674,7 +692,7 @@ pub use error::{Error, ErrorKind}; pub use matcher::Matcher; pub use mock::{IntoHeaderName, Mock}; pub use request::Request; -pub use server::Server; +pub use server::{Server, ServerOpts}; pub use server_pool::ServerGuard; mod diff; diff --git a/src/mock.rs b/src/mock.rs index 2fc7ec9..e68a6c8 100644 --- a/src/mock.rs +++ b/src/mock.rs @@ -142,10 +142,16 @@ pub struct Mock { inner: InnerMock, /// Used to warn of mocks missing a `.create()` call. See issue #112 created: bool, + assert_on_drop: bool, } impl Mock { - pub(crate) fn new>(state: Arc>, method: &str, path: P) -> Mock { + pub(crate) fn new>( + state: Arc>, + method: &str, + path: P, + assert_on_drop: bool, + ) -> Mock { let inner = InnerMock { id: thread_rng() .sample_iter(&Alphanumeric) @@ -166,6 +172,7 @@ impl Mock { state, inner, created: false, + assert_on_drop, } } @@ -356,6 +363,10 @@ impl Mock { /// /// Sets the body of the mock response dynamically. The response will use chunked transfer encoding. /// + /// The callback function will be called only once. You can sleep in between calls to the + /// writer to simulate delays between the chunks. The callback function can also return an + /// error after any number of writes in order to abort the response. + /// /// The function must be thread-safe. If it's a closure, it can't be borrowing its context. /// Use `move` closures and `Arc` to share any data. /// @@ -661,6 +672,10 @@ impl Drop for Mock { if !self.created { log::warn!("Missing .create() call on mock {}", self); } + + if self.assert_on_drop { + self.assert(); + } } } diff --git a/src/server.rs b/src/server.rs index 7a057d1..1c5e635 100644 --- a/src/server.rs +++ b/src/server.rs @@ -6,9 +6,11 @@ use crate::{Error, ErrorKind, Matcher, Mock}; use hyper::server::conn::Http; use hyper::service::service_fn; use hyper::{Body, Request as HyperRequest, Response, StatusCode}; +use std::default::Default; use std::fmt; -use std::net::SocketAddr; +use std::net::{IpAddr, SocketAddr}; use std::ops::Drop; +use std::str::FromStr; use std::sync::{mpsc, Arc, RwLock}; use std::thread; use tokio::net::TcpListener; @@ -107,6 +109,43 @@ impl State { } } +/// +/// Options to configure a mock server. Provides a default implementation. +/// +/// ``` +/// let opts = mockito::ServerOpts { port: 1234, ..Default::default() }; +/// ``` +/// +pub struct ServerOpts { + /// The server host (defaults to 127.0.0.1) + pub host: &'static str, + /// The server port (defaults to a randomly assigned free port) + pub port: u16, + /// Automatically call `assert()` before dropping a mock (defaults to false) + pub assert_on_drop: bool, +} + +impl ServerOpts { + pub(crate) fn address(&self) -> SocketAddr { + let ip = IpAddr::from_str(self.host).unwrap(); + SocketAddr::from((ip, self.port)) + } +} + +impl Default for ServerOpts { + fn default() -> Self { + let host = "127.0.0.1"; + let port = 0; + let assert_on_drop = false; + + ServerOpts { + host, + port, + assert_on_drop, + } + } +} + /// /// One instance of the mock server. /// @@ -121,16 +160,25 @@ impl State { /// let mut server = mockito::Server::new(); /// ``` /// -/// If for any reason you'd like to bypass the server pool, you can use `Server::new_with_port`: +/// If you'd like to bypass the server pool or configure the server in a different way +/// (by setting a custom host and port or enabling auto-asserts), you can use `Server::new_with_opts`: /// /// ``` -/// let mut server = mockito::Server::new_with_port(0); +/// let opts = mockito::ServerOpts { port: 0, ..Default::default() }; +/// let server_with_port = mockito::Server::new_with_opts(opts); +/// +/// let opts = mockito::ServerOpts { host: "0.0.0.0", ..Default::default() }; +/// let server_with_host = mockito::Server::new_with_opts(opts); +/// +/// let opts = mockito::ServerOpts { assert_on_drop: true, ..Default::default() }; +/// let server_with_auto_assert = mockito::Server::new_with_opts(opts); /// ``` /// #[derive(Debug)] pub struct Server { address: SocketAddr, state: Arc>, + assert_on_drop: bool, } impl Server { @@ -175,30 +223,55 @@ impl Server { } /// - /// Starts a new server on a given port. If the port is set to `0`, a random available - /// port will be assigned. Note that **this call bypasses the server pool**. + /// **DEPRECATED:** Use `Server::new_with_opts` instead. + /// + #[deprecated(since = "1.3.0", note = "Use `Server::new_with_opts` instead")] + #[track_caller] + pub fn new_with_port(port: u16) -> Server { + let opts = ServerOpts { + port, + ..Default::default() + }; + Server::try_new_with_opts(opts).unwrap() + } + + /// + /// Starts a new server with the given options. Note that **this call bypasses the server pool**. /// /// This method will panic on failure. /// #[track_caller] - pub fn new_with_port(port: u16) -> Server { - Server::try_new_with_port(port).unwrap() + pub fn new_with_opts(opts: ServerOpts) -> Server { + Server::try_new_with_opts(opts).unwrap() } /// - /// Same as `Server::new_with_port` but async. + /// **DEPRECATED:** Use `Server::new_with_opts_async` instead. /// + #[deprecated(since = "1.3.0", note = "Use `Server::new_with_opts_async` instead")] pub async fn new_with_port_async(port: u16) -> Server { - Server::try_new_with_port_async(port).await.unwrap() + let opts = ServerOpts { + port, + ..Default::default() + }; + Server::try_new_with_opts_async(opts).await.unwrap() + } + + /// + /// Same as `Server::new_with_opts` but async. + /// + pub async fn new_with_opts_async(opts: ServerOpts) -> Server { + Server::try_new_with_opts_async(opts).await.unwrap() } /// - /// Same as `Server::new_with_port` but won't panic on failure. + /// Same as `Server::new_with_opts` but won't panic on failure. /// #[track_caller] - pub(crate) fn try_new_with_port(port: u16) -> Result { + pub(crate) fn try_new_with_opts(opts: ServerOpts) -> Result { let state = Arc::new(RwLock::new(State::new())); - let address = SocketAddr::from(([127, 0, 0, 1], port)); + let address = opts.address(); + let assert_on_drop = opts.assert_on_drop; let (address_sender, address_receiver) = mpsc::channel::(); let runtime = runtime::Builder::new_current_thread() .enable_all() @@ -215,17 +288,22 @@ impl Server { .recv() .map_err(|err| Error::new_with_context(ErrorKind::ServerFailure, err))?; - let server = Server { address, state }; + let server = Server { + address, + state, + assert_on_drop, + }; Ok(server) } /// - /// Same as `Server::try_new_with_port` but async. + /// Same as `Server::try_new_with_opts` but async. /// - pub(crate) async fn try_new_with_port_async(port: u16) -> Result { + pub(crate) async fn try_new_with_opts_async(opts: ServerOpts) -> Result { let state = Arc::new(RwLock::new(State::new())); - let address = SocketAddr::from(([127, 0, 0, 1], port)); + let address = opts.address(); + let assert_on_drop = opts.assert_on_drop; let (address_sender, address_receiver) = mpsc::channel::(); let runtime = runtime::Builder::new_current_thread() .enable_all() @@ -242,7 +320,11 @@ impl Server { .recv() .map_err(|err| Error::new_with_context(ErrorKind::ServerFailure, err))?; - let server = Server { address, state }; + let server = Server { + address, + state, + assert_on_drop, + }; Ok(server) } @@ -296,7 +378,7 @@ impl Server { /// ``` /// pub fn mock>(&mut self, method: &str, path: P) -> Mock { - Mock::new(self.state.clone(), method, path) + Mock::new(self.state.clone(), method, path, self.assert_on_drop) } /// diff --git a/src/server_pool.rs b/src/server_pool.rs index 537433f..59c278a 100644 --- a/src/server_pool.rs +++ b/src/server_pool.rs @@ -1,5 +1,5 @@ -use crate::Server; use crate::{Error, ErrorKind}; +use crate::{Server, ServerOpts}; use std::collections::VecDeque; use std::ops::{Deref, DerefMut, Drop}; use std::sync::Mutex; @@ -75,7 +75,7 @@ impl ServerPool { let recycled = self.free_list.lock().unwrap().pop_front(); let server = match recycled { Some(server) => server, - None => Server::try_new_with_port_async(0).await?, + None => Server::try_new_with_opts_async(ServerOpts::default()).await?, }; Ok(ServerGuard::new(server, permit)) diff --git a/tests/lib.rs b/tests/lib.rs index 77ae92c..0fc1aef 100644 --- a/tests/lib.rs +++ b/tests/lib.rs @@ -1,7 +1,7 @@ #[macro_use] extern crate serde_json; -use mockito::{Matcher, Server}; +use mockito::{Matcher, Server, ServerOpts}; use rand::distributions::Alphanumeric; use rand::Rng; use std::fmt::Display; @@ -739,7 +739,11 @@ fn test_unpooled_server_going_out_of_context_removes_all_mocks() { let address; { - let mut s = Server::new_with_port(0); + let opts = ServerOpts { + port: 0, + ..Default::default() + }; + let mut s = Server::new_with_opts(opts); address = s.host_with_port(); s.mock("GET", "/reset").create(); @@ -1183,6 +1187,35 @@ fn test_assert_defaults_to_one_hit() { mock.assert(); } +#[test] +fn test_server_with_assert_on_drop_defaults_to_one_hit() { + let opts = ServerOpts { + assert_on_drop: true, + ..Default::default() + }; + let mut s = Server::new_with_opts(opts); + let host = s.host_with_port(); + let _mock = s.mock("GET", "/hello").create(); + + request(host, "GET /hello", ""); +} + +#[tokio::test] +async fn test_server_with_assert_on_drop_defaults_to_one_hit_async() { + let opts = ServerOpts { + assert_on_drop: true, + ..Default::default() + }; + let mut s = Server::new_with_opts_async(opts).await; + let _mock = s.mock("GET", "/hello").create_async().await; + + reqwest::Client::new() + .get(format!("{}/hello", s.url())) + .send() + .await + .unwrap(); +} + #[test] fn test_expect() { let mut s = Server::new(); @@ -1282,6 +1315,52 @@ fn test_assert_panics_expect_at_least_with_too_few_requests() { mock.assert(); } +#[test] +#[should_panic( + expected = "\n> Expected at least 3 request(s) to:\n\r\nGET /hello\r\n\n...but received 2\n" +)] +fn test_server_with_assert_on_drop_panics_expect_at_least_with_too_few_requests() { + let opts = ServerOpts { + assert_on_drop: true, + ..Default::default() + }; + let mut s = Server::new_with_opts(opts); + let host = s.host_with_port(); + let _mock = s.mock("GET", "/hello").expect_at_least(3).create(); + + request(&host, "GET /hello", ""); + request(&host, "GET /hello", ""); +} + +#[tokio::test] +#[should_panic( + expected = "\n> Expected at least 3 request(s) to:\n\r\nGET /hello\r\n\n...but received 2\n" +)] +async fn test_server_with_assert_on_drop_panics_expect_at_least_with_too_few_requests_async() { + let opts = ServerOpts { + assert_on_drop: true, + ..Default::default() + }; + let mut s = Server::new_with_opts_async(opts).await; + let _mock = s + .mock("GET", "/hello") + .expect_at_least(3) + .create_async() + .await; + + reqwest::Client::new() + .get(format!("{}/hello", s.url())) + .send() + .await + .unwrap(); + + reqwest::Client::new() + .get(format!("{}/hello", s.url())) + .send() + .await + .unwrap(); +} + #[test] #[should_panic( expected = "\n> Expected at most 3 request(s) to:\n\r\nGET /hello\r\n\n...but received 4\n" @@ -1350,6 +1429,28 @@ fn test_assert_panics_if_no_request_was_performed() { mock.assert(); } +#[test] +#[should_panic(expected = "\n> Expected 1 request(s) to:\n\r\nGET /hello\r\n\n...but received 0\n")] +fn test_server_with_assert_on_drop_panics_if_no_request_was_performed() { + let opts = ServerOpts { + assert_on_drop: true, + ..Default::default() + }; + let mut s = Server::new_with_opts(opts); + let _mock = s.mock("GET", "/hello").create(); +} + +#[tokio::test] +#[should_panic(expected = "\n> Expected 1 request(s) to:\n\r\nGET /hello\r\n\n...but received 0\n")] +async fn test_server_with_assert_on_drop_panics_if_no_request_was_performed_async() { + let opts = ServerOpts { + assert_on_drop: true, + ..Default::default() + }; + let mut s = Server::new_with_opts_async(opts).await; + let _mock = s.mock("GET", "/hello").create_async().await; +} + #[test] #[should_panic(expected = "\n> Expected 3 request(s) to:\n\r\nGET /hello\r\n\n...but received 2\n")] fn test_assert_panics_with_too_few_requests() {