diff --git a/.cargo/config.toml b/.cargo/config.toml new file mode 100644 index 0000000..8969614 --- /dev/null +++ b/.cargo/config.toml @@ -0,0 +1,2 @@ +[alias] +clippy-mockito = "clippy --lib --tests --all-features" diff --git a/.github/workflows/linters.yml b/.github/workflows/linters.yml new file mode 100644 index 0000000..29abeef --- /dev/null +++ b/.github/workflows/linters.yml @@ -0,0 +1,37 @@ +name: Linters + +on: + push: + branches: + - master + pull_request: + branches: + - "*" + +jobs: + rustfmt: + name: Run rustfmt on the minimum supported toolchain + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v1 + - uses: actions-rs/toolchain@v1 + with: + toolchain: 1.65.0 + profile: minimal + components: clippy, rustfmt + override: true + - name: Run rustfmt + run: cargo fmt -- --check + clippy: + name: Run clippy on the minimum supported toolchain + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v1 + - uses: actions-rs/toolchain@v1 + with: + toolchain: 1.65.0 + profile: minimal + components: clippy, rustfmt + override: true + - name: Run clippy + run: cargo clippy-mockito diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml new file mode 100644 index 0000000..93295ee --- /dev/null +++ b/.github/workflows/tests.yml @@ -0,0 +1,56 @@ +name: Tests + +on: + push: + branches: + - master + pull_request: + branches: + - "*" + +jobs: + test-default: + name: Test the minimum supported toolchain + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v1 + - uses: actions-rs/toolchain@v1 + with: + toolchain: 1.65.0 + profile: minimal + override: true + - name: Check + run: cargo check + - name: Test + run: cargo test --no-default-features + + test-latest: + name: Test on latest stable + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v1 + - uses: actions-rs/toolchain@v1 + with: + toolchain: stable + profile: minimal + override: true + - name: Check + run: cargo check + - name: Test + run: cargo test --no-default-features + + test-nightly: + name: Test on nightly + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v1 + - uses: actions-rs/toolchain@v1 + with: + toolchain: nightly + profile: minimal + override: true + - name: Check + run: cargo check + - name: Test + run: cargo test --no-default-features + diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index 09525f7..0000000 --- a/.travis.yml +++ /dev/null @@ -1,32 +0,0 @@ -language: rust -rust: - - 1.42.0 # minimum supported toolchain - - stable - - beta - - nightly -os: linux - -jobs: - fast_finish: true - allow_failures: - - rust: nightly - include: - - name: Lints - rust: 1.42.0 - install: - - rustup component add clippy - - rustup component add rustfmt - script: - - cargo clippy --lib --tests --all-features -- -D clippy::complexity - - cargo fmt -- --check - -branches: - only: - - master - -script: - - cargo test - -notifications: - email: - on_success: never diff --git a/Cargo.toml b/Cargo.toml index e9d89ce..3f7b5f7 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -10,27 +10,30 @@ description = "HTTP mocking for Rust." keywords = ["mock", "mocks", "http", "webmock", "webmocks"] categories = ["development-tools::testing", "web-programming"] exclude = ["/.appveyor.yml", "/.travis.yml", "/benchmarks.txt", "/docs/", "/slides.pdf"] -edition = "2018" +edition = "2021" [badges] travis-ci = { repository = "lipanski/mockito", branch = "master" } appveyor = { repository = "lipanski/mockito", branch = "master", service = "github" } [dependencies] +assert-json-diff = "2.0" +async-trait = "0.1" +colored = { version = "2.0", optional = true } +deadpool = "0.9" +futures = "0.3" +hyper = { version = "0.14", features = ["full"] } +lazy_static = "1.4" +log = "0.4" rand = "0.8" -httparse = "1.3.3" -regex = "1.0.5" -lazy_static = "1.1.0" -serde_json = "1.0.17" -similar = "2.1" - -colored = { version = "2.0.0", optional = true } -log = "0.4.6" -assert-json-diff = "2.0.0" -serde_urlencoded = "0.7.0" +regex = "1.7" +serde_json = "1.0" +serde_urlencoded = "0.7" +similar = "2.2" +tokio = { version = "1.25", features = ["full"] } [dev-dependencies] -env_logger = "0.8.2" +env_logger = "0.8" testing_logger = "0.1" [features] diff --git a/README.md b/README.md index 4a7e25f..b022705 100644 --- a/README.md +++ b/README.md @@ -3,20 +3,117 @@

- + - +

HTTP mocking for Rust!

-Get it on [crates.io](https://crates.io/crates/mockito/). +Mockito is a library for **generating and delivering HTTP mocks** in Rust. You can use it for integration testing +or offline work. Mockito runs a local pool of HTTP servers which create, deliver and remove the mocks. -Documentation is available at . +## Features + +- Support for HTTP1/2 +- Multi-threaded +- Various request matchers (Regex, JSON etc.) +- Mock multiple hosts at the same time +- Sync and async interface +- Simple, intuitive API +- An awesome logo + +The full documentation is available at . Before upgrading, make sure to check out the [changelog](https://github.com/lipanski/mockito/releases). +## Getting Started + +Add `mockito` to your `Cargo.toml` and start mocking: + +```rust +#[test] +fn test_something() { + // Request a new server from the pool + let mut server = mockito::Server::new(); + + // Use one of these addresses to configure your client + let host = server.host_with_port(); + let url = server.url(); + + // Create a mock + let m = server.mock("GET", "/hello") + .with_status(201) + .with_header("content-type", "text/plain") + .with_header("x-api-key", "1234") + .with_body("world") + .create(); + + // Any calls to GET /hello beyond this line will respond with 201, the + // `content-type: text/plain` header and the body "world". + + // You can use `Mock::assert` to verify that your mock was called + m.assert(); +} +``` + +Use **matchers** to handle requests to the same endpoint in a different way: + +```rust +#[test] +fn test_something() { + let mut server = mockito::Server::new(); + + let m1 = server.mock("GET", "/greetings") + .match_header("content-type", "application/json") + .match_body(mockito::Matcher::PartialJsonString( + "{\"greeting\": \"hello\"}".to_string(), + )) + .with_body("hello json") + .create(); + + let m2 = server.mock("GET", "/greetings") + .match_header("content-type", "application/text") + .match_body(mockito::Matcher::Regex("greeting=hello".to_string())) + .with_body("hello text") + .create(); +} +``` + +Start **multiple servers** to simulate requests to different hosts: + +```rust +#[test] +fn test_something() { + let mut twitter = mockito::Server::new(); + let mut github = mockito::Server::new(); + + // These mocks will be available at `twitter.url()` + let twitter_mock = twitter.mock("GET", "/api").create(); + + // These mocks will be available at `github.url()` + let github_mock = github.mock("GET", "/api").create(); +} +``` + +Write **async** tests: + +```rust +#[tokio::test(flavor = "multi_thread")] +async fn test_simple_route_mock_async() { + let mut server = Server::new_async().await; + let _m1 = server.mock("GET", "/a").with_body("aaa").create_async(); + let _m2 = server.mock("GET", "/b").with_body("bbb").create_async(); + + let (_m1, _m2) = futures::join!(_m1, _m2); +} +``` + +## Minimum supported Rust toolchain + +The current minimum support Rust toolchain is **1.65.0** + ## Contribution Guidelines 1. Check the existing issues and pull requests. @@ -37,7 +134,7 @@ cargo test ...or run tests using a different toolchain: ```sh -rustup run --install 1.42.0 cargo test +rustup run --install 1.65.0 cargo test ``` ...or run tests while disabling the default features (e.g. the colors): @@ -71,13 +168,13 @@ Mockito uses [clippy](https://github.com/rust-lang/rust-clippy) and it should be Install `clippy`: ```sh -rustup component add clippy-preview +rustup component add clippy ``` -Run the linter on the minimum supported Rust version: +The linter is always run on the minimum supported Rust version: ```sh -rustup run --install 1.42.0 cargo clippy --lib --tests --all-features -- -D clippy::complexity +rustup run --install 1.65.0 cargo clippy-mockito ``` ### Release @@ -101,7 +198,3 @@ Run benchmarks: ```sh rustup run nightly cargo bench ``` - ---- - -Logo courtesy to [http://niastudio.net](http://niastudio.net) :ok_hand: diff --git a/benches/lib.rs b/benches/lib.rs index 4e62f5e..1225b37 100644 --- a/benches/lib.rs +++ b/benches/lib.rs @@ -2,14 +2,15 @@ extern crate test; -use mockito::{mock, reset, server_address}; +use mockito::Server; +use std::fmt::Display; use std::io::{BufRead, BufReader, Read, Write}; use std::net::TcpStream; use std::str::FromStr; use test::Bencher; -fn request_stream(route: &str, headers: &str) -> TcpStream { - let mut stream = TcpStream::connect(server_address()).unwrap(); +fn request_stream(host: impl Display, route: &str, headers: &str) -> TcpStream { + let mut stream = TcpStream::connect(host.to_string()).unwrap(); let message = [route, " HTTP/1.1\r\n", headers, "\r\n"].join(""); stream.write_all(message.as_bytes()).unwrap(); @@ -49,27 +50,27 @@ fn parse_stream(stream: TcpStream) -> (String, Vec, String) { (status_line, headers, body) } -fn request(route: &str, headers: &str) -> (String, Vec, String) { - parse_stream(request_stream(route, headers)) +fn request(host: impl Display, route: &str, headers: &str) -> (String, Vec, String) { + parse_stream(request_stream(host, route, headers)) } #[bench] fn bench_create_simple_mock(b: &mut Bencher) { - reset(); + let mut s = Server::new(); b.iter(|| { - let _m = mock("GET", "/").with_body("test").create(); + let _m = s.mock("GET", "/").with_body("test").create(); }) } #[bench] fn bench_match_simple_mock(b: &mut Bencher) { - reset(); + let mut s = Server::new(); - let _m = mock("GET", "/").with_body("test").create(); + let _m = s.mock("GET", "/").with_body("test").create(); b.iter(|| { - let (status_line, _, _) = request("GET /", ""); + let (status_line, _, _) = request(&s.host_with_port(), "GET /", ""); assert!(status_line.starts_with("HTTP/1.1 200")); }) } diff --git a/examples/mockito-server.rs b/examples/mockito-server.rs index 857f34b..0dc5361 100644 --- a/examples/mockito-server.rs +++ b/examples/mockito-server.rs @@ -3,7 +3,9 @@ use mockito; use std::time::Duration; fn main() { - mockito::start(); + let mut s = mockito::Server::new(); + + s.mock("GET", "/").with_body("hello world"); loop { std::thread::sleep(Duration::from_secs(1)) diff --git a/rustfmt.toml b/rustfmt.toml new file mode 100644 index 0000000..f42c8b3 --- /dev/null +++ b/rustfmt.toml @@ -0,0 +1,2 @@ +edition = "2021" +max_width = 100 diff --git a/src/command.rs b/src/command.rs new file mode 100644 index 0000000..f6d6a32 --- /dev/null +++ b/src/command.rs @@ -0,0 +1,119 @@ +use crate::server::{RemoteMock, State}; +use tokio::sync::mpsc::Sender; +use tokio::sync::oneshot; +use tokio::sync::MutexGuard; + +#[derive(Debug)] +pub(crate) enum Command { + CreateMock { + remote_mock: RemoteMock, + response_sender: oneshot::Sender, + }, + GetMockHits { + mock_id: String, + response_sender: oneshot::Sender>, + }, + RemoveMock { + mock_id: String, + response_sender: oneshot::Sender, + }, + GetLastUnmatchedRequest { + response_sender: oneshot::Sender>, + }, +} + +impl Command { + pub(crate) async fn create_mock(sender: &Sender, remote_mock: RemoteMock) -> bool { + let (response_sender, response_receiver) = oneshot::channel(); + + let cmd = Command::CreateMock { + remote_mock, + response_sender, + }; + + let _ = sender.send(cmd).await; + response_receiver.await.unwrap_or(false) + } + + pub(crate) async fn get_mock_hits(sender: &Sender, mock_id: String) -> Option { + let (response_sender, response_receiver) = oneshot::channel(); + + let cmd = Command::GetMockHits { + mock_id, + response_sender, + }; + + let _ = sender.send(cmd).await; + response_receiver.await.unwrap() + } + + pub(crate) async fn remove_mock(sender: &Sender, mock_id: String) -> bool { + let (response_sender, response_receiver) = oneshot::channel(); + + let cmd = Command::RemoveMock { + mock_id, + response_sender, + }; + + let _ = sender.send(cmd).await; + response_receiver.await.unwrap_or(false) + } + + pub(crate) async fn get_last_unmatched_request(sender: &Sender) -> Option { + let (response_sender, response_receiver) = oneshot::channel(); + + let cmd = Command::GetLastUnmatchedRequest { response_sender }; + + let _ = sender.send(cmd).await; + response_receiver.await.unwrap_or(None) + } + + pub async fn handle(cmd: Command, mut state: MutexGuard<'_, State>) { + match cmd { + Command::CreateMock { + remote_mock, + response_sender, + } => { + state.mocks.push(remote_mock); + + let _ = response_sender.send(true); + } + Command::GetMockHits { + mock_id, + response_sender, + } => { + let hits: Option = state + .mocks + .iter() + .find(|remote_mock| remote_mock.inner.id == mock_id) + .map(|remote_mock| remote_mock.inner.hits); + + let _ = response_sender.send(hits); + } + Command::RemoveMock { + mock_id, + response_sender, + } => { + if let Some(pos) = state + .mocks + .iter() + .position(|remote_mock| remote_mock.inner.id == mock_id) + { + state.mocks.remove(pos); + } + + let _ = response_sender.send(true); + } + Command::GetLastUnmatchedRequest { response_sender } => { + let last_unmatched_request = state.unmatched_requests.last_mut(); + + let label = match last_unmatched_request { + Some(req) => Some(req.to_string().await), + None => None, + }; + + let _ = response_sender.send(label); + } + } + } +} diff --git a/src/diff.rs b/src/diff.rs index b1eab1d..540e81e 100644 --- a/src/diff.rs +++ b/src/diff.rs @@ -22,6 +22,7 @@ pub fn compare(expected: &str, actual: &str) -> String { ChangeTag::Equal => { let z = change.value(); #[cfg(feature = "color")] + #[allow(clippy::unnecessary_to_owned)] result.push_str(&z.green().to_string()); #[cfg(not(feature = "color"))] result.push_str(z); @@ -29,6 +30,7 @@ pub fn compare(expected: &str, actual: &str) -> String { ChangeTag::Insert => { let z = change.value(); #[cfg(feature = "color")] + #[allow(clippy::unnecessary_to_owned)] result.push_str(&z.white().on_green().to_string()); #[cfg(not(feature = "color"))] result.push_str(z); @@ -38,6 +40,7 @@ pub fn compare(expected: &str, actual: &str) -> String { } } else { #[cfg(feature = "color")] + #[allow(clippy::unnecessary_to_owned)] result.push_str(&x.bright_green().to_string()); #[cfg(not(feature = "color"))] result.push_str(x); @@ -45,6 +48,7 @@ pub fn compare(expected: &str, actual: &str) -> String { } ChangeTag::Delete => { #[cfg(feature = "color")] + #[allow(clippy::unnecessary_to_owned)] result.push_str(&x.red().to_string()); #[cfg(not(feature = "color"))] result.push_str(x); diff --git a/src/error.rs b/src/error.rs new file mode 100644 index 0000000..049090c --- /dev/null +++ b/src/error.rs @@ -0,0 +1,74 @@ +use std::error::Error as ErrorTrait; +use std::fmt::Display; + +/// +/// Contains information about an error occurence +/// +#[derive(Debug)] +pub struct Error { + /// The type of this error + pub kind: ErrorKind, + /// Some errors come with more context + pub context: Option, +} + +impl Error { + pub(crate) fn new(kind: ErrorKind) -> Error { + Error { + kind, + context: None, + } + } + + pub(crate) fn new_with_context(kind: ErrorKind, context: impl Display) -> Error { + Error { + kind, + context: Some(context.to_string()), + } + } +} + +impl Display for Error { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!( + f, + "{} (context: {})", + self.kind.description(), + self.context.as_ref().unwrap_or(&"none".to_string()) + ) + } +} + +impl ErrorTrait for Error {} + +/// +/// The type of an error +/// +#[derive(Debug)] +pub enum ErrorKind { + /// The server is not running + ServerFailure, + /// Could not deliver a response + ResponseFailure, + /// The status code is invalid or out of range + InvalidStatusCode, + /// Failed to read the request body + RequestBodyFailure, + /// Failed to write the response body + ResponseBodyFailure, + /// File not found + FileNotFound, +} + +impl ErrorKind { + fn description(&self) -> &'static str { + match self { + ErrorKind::ServerFailure => "the server is not running", + ErrorKind::ResponseFailure => "could not deliver a response", + ErrorKind::InvalidStatusCode => "invalid status code", + ErrorKind::RequestBodyFailure => "failed to read the request body", + ErrorKind::ResponseBodyFailure => "failed to write the response body", + ErrorKind::FileNotFound => "file not found", + } + } +} diff --git a/src/legacy.rs b/src/legacy.rs new file mode 100644 index 0000000..3887a78 --- /dev/null +++ b/src/legacy.rs @@ -0,0 +1,95 @@ +use crate::{Matcher, Mock, Server}; +use lazy_static::lazy_static; +use std::cell::RefCell; +use std::sync::LockResult; +use std::sync::{Mutex, MutexGuard}; +lazy_static! { + // Legacy mode. + // A global lock that ensure all Mockito tests are run on a single thread. + static ref TEST_MUTEX: Mutex<()> = Mutex::new(()); + + // Legacy mode. + static ref DEFAULT_SERVER: Mutex = Mutex::new(Server::new_with_port(0)); +} +thread_local!( + // Legacy mode. + // A thread-local reference to the global lock. This is acquired within `mock()`. + pub(crate) static LOCAL_TEST_MUTEX: RefCell>> = + RefCell::new(TEST_MUTEX.lock()); +); + +/// +/// **DEPRECATED:** This method is part of the legacy interface an will be removed +/// in future versions. You should replace it with `Server::mock`: +/// +/// ``` +/// let mut s = mockito::Server::new(); +/// let _m1 = s.mock("GET", "/"); +/// ``` +/// +/// Initializes a mock with the given HTTP `method` and `path`. +/// +/// The mock is registered to the server only after the `create()` method has been called. +/// +#[deprecated(since = "0.32.0", note = "Use `Server::mock` instead")] +pub fn mock>(method: &str, path: P) -> Mock { + // Legacy mode. + // Ensures Mockito tests are run sequentially. + LOCAL_TEST_MUTEX.with(|_| {}); + + let mut server = DEFAULT_SERVER.lock().unwrap(); + + server.mock(method, path) +} + +/// +/// **DEPRECATED:** This method is part of the legacy interface an will be removed +/// in future versions. You should replace it with `Server::host_with_port`: +/// +/// ``` +/// let mut s = mockito::Server::new(); +/// let server_address = s.host_with_port(); +/// ``` +/// +/// The host and port of the local server. +/// Can be used with `std::net::TcpStream`. +/// +#[deprecated(since = "0.32.0", note = "Use `Server::host_with_port` instead")] +pub fn server_address() -> String { + let server = DEFAULT_SERVER.lock().unwrap(); + server.host_with_port() +} + +/// +/// **DEPRECATED:** This method is part of the legacy interface an will be removed +/// in future versions. You should replace it with `Server::url`: +/// +/// ``` +/// let mut s = mockito::Server::new(); +/// let server_url = s.url(); +/// ``` +/// +/// The local `http://...` URL of the server. +/// +#[deprecated(since = "0.32.0", note = "Use `Server::url` instead")] +pub fn server_url() -> String { + let server = DEFAULT_SERVER.lock().unwrap(); + server.url() +} + +/// +/// **DEPRECATED:** This method is part of the legacy interface an will be removed +/// in future versions. You should replace it with `Server::reset`: +/// +/// ``` +/// let mut s = mockito::Server::new(); +/// s.reset(); +/// ``` +/// +/// Removes all the mocks stored on the server. +/// +#[deprecated(since = "0.32.0", note = "Use `Server::reset` instead")] +pub fn reset() { + let mut server = DEFAULT_SERVER.lock().unwrap(); + server.reset(); +} diff --git a/src/lib.rs b/src/lib.rs index 4939865..06ee921 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -4,46 +4,37 @@ )] //! -//! Mockito is a library for creating HTTP mocks to be used in integration tests or for offline work. -//! It runs an HTTP server on a local port which delivers, creates and remove the mocks. +//! Mockito is a library for **generating and delivering HTTP mocks** in Rust. You can use it for integration testing +//! or offline work. Mockito runs a local pool of HTTP servers which create, deliver and remove the mocks. //! -//! The server is run on a separate thread within the same process and will be removed -//! at the end of the run. +//! # Features //! -//! # Getting Started -//! -//! Use `mockito::server_url()` or `mockito::server_address()` as the base URL for any mocked -//! client in your tests. One way to do this is by using compiler flags: -//! -//! ## Example -//! -//! ``` -//! #[cfg(test)] -//! use mockito; -//! -//! fn main() { -//! #[cfg(not(test))] -//! let url = "https://api.twitter.com"; +//! - Support for HTTP1/2 +//! - Multi-threaded +//! - Various request matchers (Regex, JSON etc.) +//! - Mock multiple hosts at the same time +//! - Sync and async interface +//! - Simple, intuitive API +//! - An awesome logo //! -//! #[cfg(test)] -//! let url = &mockito::server_url(); -//! -//! // Use url as the base URL for your client -//! } -//! ``` -//! -//! Then start mocking: +//! # Getting Started //! -//! ## Example +//! Add `mockito` to your `Cargo.toml` and start mocking: //! //! ``` //! #[cfg(test)] //! mod tests { -//! use mockito::mock; -//! //! #[test] //! fn test_something() { -//! let _m = mock("GET", "/hello") +//! // Request a new server from the pool +//! let mut server = mockito::Server::new(); +//! +//! // Use one of these addresses to configure your client +//! let host = server.host_with_port(); +//! let url = server.url(); +//! +//! // Create a mock +//! let _m = server.mock("GET", "/hello") //! .with_status(201) //! .with_header("content-type", "text/plain") //! .with_header("x-api-key", "1234") @@ -52,215 +43,105 @@ //! //! // Any calls to GET /hello beyond this line will respond with 201, the //! // `content-type: text/plain` header and the body "world". +//! +//! // You can use `Mock::assert` to verify that your mock was called +//! // m.assert(); //! } //! } //! ``` //! -//! # Lifetime -//! -//! Just like any Rust object, a mock is available only through its lifetime. You'll want to assign -//! the mocks to variables in order to extend and control their lifetime. -//! -//! Avoid using the underscore matcher when creating your mocks, as in `let _ = mock("GET", "/")`. -//! This will end your mock's lifetime immediately. You can still use the underscore to prefix your variable -//! names in an assignment, but don't limit it to just this one character. -//! -//! ## Example +//! Use **matchers** to handle requests to the same endpoint in a different way: //! //! ``` -//! use mockito::mock; -//! -//! let _m1 = mock("GET", "/long").with_body("hello").create(); -//! -//! { -//! let _m2 = mock("GET", "/short").with_body("hi").create(); -//! -//! // Requests to GET /short will be mocked til here -//! } -//! -//! // Requests to GET /long will be mocked til here -//! ``` -//! -//! # Limitations -//! -//! Creating mocks from threads is currently not possible. Please use the main (test) thread for that. -//! See the note on threads at the end for more details. -//! -//! # Asserts -//! -//! You can use the `Mock::assert` method to **assert that a mock was called**. In other words, the -//! `Mock#assert` method can validate that your code performed the expected HTTP requests. -//! -//! By default, the method expects that only one request to your mock was triggered. -//! -//! ## Example -//! -//! ```no_run -//! use std::net::TcpStream; -//! use std::io::{Read, Write}; -//! use mockito::{mock, server_address}; -//! -//! let mock = mock("GET", "/hello").create(); +//! #[cfg(test)] +//! mod tests { +//! #[test] +//! fn test_something() { +//! let mut server = mockito::Server::new(); +//! +//! let m1 = server.mock("GET", "/greetings") +//! .match_header("content-type", "application/json") +//! .match_body(mockito::Matcher::PartialJsonString( +//! "{\"greeting\": \"hello\"}".to_string(), +//! )) +//! .with_body("hello json") +//! .create(); //! -//! { -//! // Place a request -//! let mut stream = TcpStream::connect(server_address()).unwrap(); -//! stream.write_all("GET /hello HTTP/1.1\r\n\r\n".as_bytes()).unwrap(); -//! let mut response = String::new(); -//! stream.read_to_string(&mut response).unwrap(); -//! stream.flush().unwrap(); +//! let m2 = server.mock("GET", "/greetings") +//! .match_header("content-type", "application/text") +//! .match_body(mockito::Matcher::Regex("greeting=hello".to_string())) +//! .with_body("hello text") +//! .create(); +//! } //! } -//! -//! mock.assert(); //! ``` //! -//! When several mocks can match a request, Mockito applies the first one that still expects requests. -//! You can use this behaviour to provide **different responses for subsequent requests to the same endpoint**. -//! -//! ## Example +//! Start **multiple servers** to simulate requests to different hosts: //! //! ``` -//! use std::net::TcpStream; -//! use std::io::{Read, Write}; -//! use mockito::{mock, server_address}; -//! -//! let english_hello_mock = mock("GET", "/hello").with_body("good bye").create(); -//! let french_hello_mock = mock("GET", "/hello").with_body("au revoir").create(); -//! -//! { -//! // Place a request to GET /hello -//! let mut stream = TcpStream::connect(server_address()).unwrap(); -//! stream.write_all("GET /hello HTTP/1.1\r\n\r\n".as_bytes()).unwrap(); -//! let mut response = String::new(); -//! stream.read_to_string(&mut response).unwrap(); -//! stream.flush().unwrap(); -//! } +//! #[cfg(test)] +//! mod tests { +//! #[test] +//! fn test_something() { +//! let mut twitter = mockito::Server::new(); +//! let mut github = mockito::Server::new(); //! -//! english_hello_mock.assert(); +//! // These mocks will be available at `twitter.url()` +//! let twitter_mock = twitter.mock("GET", "/api").create(); //! -//! { -//! // Place another request to GET /hello -//! let mut stream = TcpStream::connect(server_address()).unwrap(); -//! stream.write_all("GET /hello HTTP/1.1\r\n\r\n".as_bytes()).unwrap(); -//! let mut response = String::new(); -//! stream.read_to_string(&mut response).unwrap(); -//! stream.flush().unwrap(); +//! // These mocks will be available at `github.url()` +//! let github_mock = github.mock("GET", "/api").create(); +//! } //! } -//! -//! french_hello_mock.assert(); //! ``` //! -//! If you're expecting more than 1 request, you can use the `Mock::expect` method to specify the exact amount of requests: -//! -//! ## Example -//! -//! ```no_run -//! use std::net::TcpStream; -//! use std::io::{Read, Write}; -//! use mockito::{mock, server_address}; -//! -//! let mock = mockito::mock("GET", "/hello").expect(3).create(); -//! -//! for _ in 0..3 { -//! // Place a request -//! let mut stream = TcpStream::connect(server_address()).unwrap(); -//! stream.write_all("GET /hello HTTP/1.1\r\n\r\n".as_bytes()).unwrap(); -//! let mut response = String::new(); -//! stream.read_to_string(&mut response).unwrap(); -//! stream.flush().unwrap(); -//! } +//! Write **async** tests: //! -//! mock.assert(); //! ``` +//! #[cfg(test)] +//! mod tests { +//! #[tokio::test(flavor = "multi_thread")] +//! async fn test_something() { +//! let mut server = Server::new_async().await; +//! let _m1 = server.mock("GET", "/a").with_body("aaa").create_async(); +//! let _m2 = server.mock("GET", "/b").with_body("bbb").create_async(); //! -//! You can also work with ranges, by using the `Mock::expect_at_least` and `Mock::expect_at_most` methods: -//! -//! ## Example -//! -//! ```no_run -//! use std::net::TcpStream; -//! use std::io::{Read, Write}; -//! use mockito::{mock, server_address}; -//! -//! let mock = mockito::mock("GET", "/hello").expect_at_least(2).expect_at_most(4).create(); -//! -//! for _ in 0..3 { -//! // Place a request -//! let mut stream = TcpStream::connect(server_address()).unwrap(); -//! stream.write_all("GET /hello HTTP/1.1\r\n\r\n".as_bytes()).unwrap(); -//! let mut response = String::new(); -//! stream.read_to_string(&mut response).unwrap(); -//! stream.flush().unwrap(); +//! let (_m1, _m2) = futures::join!(_m1, _m2); +//! } //! } -//! -//! mock.assert(); //! ``` //! -//! The errors produced by the `assert` method contain information about the tested mock, but also about the -//! **last unmatched request**, which can be very useful to track down an error in your implementation or -//! a missing or incomplete mock. A colored diff is also displayed. -//! -//! Color output is enabled by default, but can be toggled with the `color` feature flag. -//! -//! Here's an example of how a `Mock#assert` error looks like: -//! -//! ```text -//! > Expected 1 request(s) to: -//! -//! POST /users?number=one -//! bob -//! -//! ...but received 0 -//! -//! > The last unmatched request was: -//! -//! POST /users?number=two -//! content-length: 5 -//! alice -//! -//! > Difference: -//! -//! # A colored diff +//! # Lifetime //! -//! ``` +//! Just like any Rust object, a mock is available only through its lifetime. You'll want to assign +//! the mocks to variables in order to extend and control their lifetime. //! -//! You can also use the `matched` method to return a boolean for whether the mock was called the -//! correct number of times without panicking +//! Avoid using the underscore matcher when creating your mocks, as in `let _ = mock("GET", "/")`. +//! This will end your mock's lifetime immediately. You can still use the underscore to prefix your variable +//! names in an assignment, but don't limit it to just this one character. //! //! ## Example //! //! ``` -//! use std::net::TcpStream; -//! use std::io::{Read, Write}; -//! use mockito::{mock, server_address}; -//! -//! let mock = mock("GET", "/").create(); +//! let mut s = mockito::Server::new(); +//! let _m1 = s.mock("GET", "/long").with_body("hello").create(); //! //! { -//! let mut stream = TcpStream::connect(server_address()).unwrap(); -//! stream.write_all("GET / HTTP/1.1\r\n\r\n".as_bytes()).unwrap(); -//! let mut response = String::new(); -//! stream.read_to_string(&mut response).unwrap(); -//! stream.flush().unwrap(); -//! } +//! let _m2 = s.mock("GET", "/short").with_body("hi").create(); //! -//! assert!(mock.matched()); -//! -//! { -//! let mut stream = TcpStream::connect(server_address()).unwrap(); -//! stream.write_all("GET / HTTP/1.1\r\n\r\n".as_bytes()).unwrap(); -//! let mut response = String::new(); -//! stream.read_to_string(&mut response).unwrap(); -//! stream.flush().unwrap(); +//! // Requests to GET /short will be mocked til here //! } -//! assert!(!mock.matched()); +//! +//! // Requests to GET /long will be mocked til here //! ``` //! //! # Matchers //! //! Mockito can match your request by method, path, query, headers or body. //! -//! Various matchers are provided by the `Matcher` type: exact, partial (regular expressions), any or missing. +//! Various matchers are provided by the `Matcher` type: exact (string, binary, JSON), partial (regular expressions, +//! JSON), any or missing. The following guide will walk you through the most common matchers. Check the +//! `Matcher` documentation for all the rest. //! //! # Matching by path //! @@ -269,10 +150,10 @@ //! ## Example //! //! ``` -//! use mockito::mock; +//! let mut s = mockito::Server::new(); //! //! // Matched only calls to GET /hello -//! let _m = mock("GET", "/hello").create(); +//! let _m = s.mock("GET", "/hello").create(); //! ``` //! //! You can also match the path partially, by using a regular expression: @@ -280,10 +161,12 @@ //! ## Example //! //! ``` -//! use mockito::{mock, Matcher}; +//! let mut s = mockito::Server::new(); //! //! // Will match calls to GET /hello/1 and GET /hello/2 -//! let _m = mock("GET", Matcher::Regex(r"^/hello/(1|2)$".to_string())).create(); +//! let _m = s.mock("GET", +//! mockito::Matcher::Regex(r"^/hello/(1|2)$".to_string()) +//! ).create(); //! ``` //! //! Or you can catch all requests, by using the `Matcher::Any` variant: @@ -291,10 +174,10 @@ //! ## Example //! //! ``` -//! use mockito::{mock, Matcher}; +//! let mut s = mockito::Server::new(); //! //! // Will match any GET request -//! let _m = mock("GET", Matcher::Any).create(); +//! let _m = s.mock("GET", mockito::Matcher::Any).create(); //! ``` //! //! # Matching by query @@ -305,26 +188,26 @@ //! ## Example //! //! ``` -//! use mockito::{mock, Matcher}; +//! let mut s = mockito::Server::new(); //! //! // This will match requests containing the URL-encoded //! // query parameter `greeting=good%20day` -//! let _m1 = mock("GET", "/test") -//! .match_query(Matcher::UrlEncoded("greeting".into(), "good day".into())) +//! let _m1 = s.mock("GET", "/test") +//! .match_query(mockito::Matcher::UrlEncoded("greeting".into(), "good day".into())) //! .create(); //! //! // This will match requests containing the URL-encoded //! // query parameters `hello=world` and `greeting=good%20day` -//! let _m2 = mock("GET", "/test") -//! .match_query(Matcher::AllOf(vec![ -//! Matcher::UrlEncoded("hello".into(), "world".into()), -//! Matcher::UrlEncoded("greeting".into(), "good day".into()) +//! let _m2 = s.mock("GET", "/test") +//! .match_query(mockito::Matcher::AllOf(vec![ +//! mockito::Matcher::UrlEncoded("hello".into(), "world".into()), +//! mockito::Matcher::UrlEncoded("greeting".into(), "good day".into()) //! ])) //! .create(); //! //! // You can achieve similar results with the regex matcher -//! let _m3 = mock("GET", "/test") -//! .match_query(Matcher::Regex("hello=world".into())) +//! let _m3 = s.mock("GET", "/test") +//! .match_query(mockito::Matcher::Regex("hello=world".into())) //! .create(); //! ``` //! @@ -349,14 +232,14 @@ //! ## Example //! //! ``` -//! use mockito::mock; +//! let mut s = mockito::Server::new(); //! -//! let _m1 = mock("GET", "/hello") +//! let _m1 = s.mock("GET", "/hello") //! .match_header("content-type", "application/json") //! .with_body(r#"{"hello": "world"}"#) //! .create(); //! -//! let _m2 = mock("GET", "/hello") +//! let _m2 = s.mock("GET", "/hello") //! .match_header("content-type", "text/plain") //! .with_body("world") //! .create(); @@ -370,10 +253,10 @@ //! ## Example //! //! ``` -//! use mockito::{mock, Matcher}; +//! let mut s = mockito::Server::new(); //! -//! let _m = mock("GET", "/hello") -//! .match_header("content-type", Matcher::Regex(r".*json.*".to_string())) +//! let _m = s.mock("GET", "/hello") +//! .match_header("content-type", mockito::Matcher::Regex(r".*json.*".to_string())) //! .with_body(r#"{"hello": "world"}"#) //! .create(); //! ``` @@ -383,10 +266,10 @@ //! ## Example //! //! ``` -//! use mockito::{mock, Matcher}; +//! let mut s = mockito::Server::new(); //! -//! let _m = mock("GET", "/hello") -//! .match_header("content-type", Matcher::Any) +//! let _m = s.mock("GET", "/hello") +//! .match_header("content-type", mockito::Matcher::Any) //! .with_body("something") //! .create(); //! @@ -400,10 +283,10 @@ //! ## Example //! //! ``` -//! use mockito::{mock, Matcher}; +//! let mut s = mockito::Server::new(); //! -//! let _m = mock("GET", "/hello") -//! .match_header("authorization", Matcher::Missing) +//! let _m = s.mock("GET", "/hello") +//! .match_header("authorization", mockito::Matcher::Missing) //! .with_body("no authorization header") //! .create(); //! @@ -421,10 +304,10 @@ //! ## Example //! //! ``` -//! use mockito::mock; +//! let mut s = mockito::Server::new(); //! //! // Will match requests to POST / whenever the request body is "hello" -//! let _m = mock("POST", "/").match_body("hello").create(); +//! let _m = s.mock("POST", "/").match_body("hello").create(); //! ``` //! //! Or you can match the body by using a regular expression: @@ -432,10 +315,12 @@ //! ## Example //! //! ``` -//! use mockito::{mock, Matcher}; +//! let mut s = mockito::Server::new(); //! //! // Will match requests to POST / whenever the request body *contains* the word "hello" (e.g. "hello world") -//! let _m = mock("POST", "/").match_body(Matcher::Regex("hello".to_string())).create(); +//! let _m = s.mock("POST", "/").match_body( +//! mockito::Matcher::Regex("hello".to_string()) +//! ).create(); //! ``` //! //! Or you can match the body using a JSON object: @@ -446,11 +331,11 @@ //! # extern crate mockito; //! #[macro_use] //! extern crate serde_json; -//! use mockito::{mock, Matcher}; //! //! # fn main() { +//! let mut s = mockito::Server::new(); //! // Will match requests to POST / whenever the request body matches the json object -//! let _m = mock("POST", "/").match_body(Matcher::Json(json!({"hello": "world"}))).create(); +//! let _m = s.mock("POST", "/").match_body(mockito::Matcher::Json(json!({"hello": "world"}))).create(); //! # } //! ``` //! @@ -458,12 +343,12 @@ //! but by passing a `String` to the matcher: //! //! ``` -//! use mockito::{mock, Matcher}; +//! let mut s = mockito::Server::new(); //! //! // Will match requests to POST / whenever the request body matches the json object -//! let _m = mock("POST", "/") +//! let _m = s.mock("POST", "/") //! .match_body( -//! Matcher::JsonString(r#"{"hello": "world"}"#.to_string()) +//! mockito::Matcher::JsonString(r#"{"hello": "world"}"#.to_string()) //! ) //! .create(); //! ``` @@ -476,18 +361,18 @@ //! ## Example //! //! ``` -//! use mockito::{mock, Matcher}; +//! let mut s = mockito::Server::new(); //! //! // Will match requests to POST / whenever the request body is either `hello=world` or `{"hello":"world"}` -//! let _m = mock("POST", "/") +//! let _m = s.mock("POST", "/") //! .match_body( -//! Matcher::AnyOf(vec![ -//! Matcher::Exact("hello=world".to_string()), -//! Matcher::JsonString(r#"{"hello": "world"}"#.to_string()), +//! mockito::Matcher::AnyOf(vec![ +//! mockito::Matcher::Exact("hello=world".to_string()), +//! mockito::Matcher::JsonString(r#"{"hello": "world"}"#.to_string()), //! ]) //! ) //! .create(); -//!``` +//! ``` //! //! # The `AllOf` matcher //! @@ -497,18 +382,191 @@ //! ## Example //! //! ``` -//! use mockito::{mock, Matcher}; +//! let mut s = mockito::Server::new(); //! //! // Will match requests to POST / whenever the request body contains both `hello` and `world` -//! let _m = mock("POST", "/") +//! let _m = s.mock("POST", "/") //! .match_body( -//! Matcher::AllOf(vec![ -//! Matcher::Regex("hello".to_string()), -//! Matcher::Regex("world".to_string()), +//! mockito::Matcher::AllOf(vec![ +//! mockito::Matcher::Regex("hello".to_string()), +//! mockito::Matcher::Regex("world".to_string()), //! ]) //! ) //! .create(); -//!``` +//! ``` +//! +//! # Asserts +//! +//! You can use the `Mock::assert` method to **assert that a mock was called**. In other words, +//! `Mock#assert` can validate that your code performed the expected HTTP request. +//! +//! By default, the method expects only **one** request to your mock. +//! +//! ## Example +//! +//! ```no_run +//! use std::net::TcpStream; +//! use std::io::{Read, Write}; +//! +//! let mut s = mockito::Server::new(); +//! let mock = s.mock("GET", "/hello").create(); +//! +//! { +//! // Place a request +//! let mut stream = TcpStream::connect(s.host_with_port()).unwrap(); +//! stream.write_all("GET /hello HTTP/1.1\r\n\r\n".as_bytes()).unwrap(); +//! let mut response = String::new(); +//! stream.read_to_string(&mut response).unwrap(); +//! stream.flush().unwrap(); +//! } +//! +//! mock.assert(); +//! ``` +//! +//! When several mocks can match a request, Mockito applies the first one that still expects requests. +//! You can use this behaviour to provide **different responses for subsequent requests to the same endpoint**. +//! +//! ## Example +//! +//! ``` +//! use std::net::TcpStream; +//! use std::io::{Read, Write}; +//! +//! let mut s = mockito::Server::new(); +//! let english_hello_mock = s.mock("GET", "/hello").with_body("good bye").create(); +//! let french_hello_mock = s.mock("GET", "/hello").with_body("au revoir").create(); +//! +//! { +//! // Place a request to GET /hello +//! let mut stream = TcpStream::connect(s.host_with_port()).unwrap(); +//! stream.write_all("GET /hello HTTP/1.1\r\n\r\n".as_bytes()).unwrap(); +//! let mut response = String::new(); +//! stream.read_to_string(&mut response).unwrap(); +//! stream.flush().unwrap(); +//! } +//! +//! english_hello_mock.assert(); +//! +//! { +//! // Place another request to GET /hello +//! let mut stream = TcpStream::connect(s.host_with_port()).unwrap(); +//! stream.write_all("GET /hello HTTP/1.1\r\n\r\n".as_bytes()).unwrap(); +//! let mut response = String::new(); +//! stream.read_to_string(&mut response).unwrap(); +//! stream.flush().unwrap(); +//! } +//! +//! french_hello_mock.assert(); +//! ``` +//! +//! If you're expecting more than 1 request, you can use the `Mock::expect` method to specify the exact amount of requests: +//! +//! ## Example +//! +//! ```no_run +//! use std::net::TcpStream; +//! use std::io::{Read, Write}; +//! +//! let mut s = mockito::Server::new(); +//! +//! let mock = s.mock("GET", "/hello").expect(3).create(); +//! +//! for _ in 0..3 { +//! // Place a request +//! let mut stream = TcpStream::connect(s.host_with_port()).unwrap(); +//! stream.write_all("GET /hello HTTP/1.1\r\n\r\n".as_bytes()).unwrap(); +//! let mut response = String::new(); +//! stream.read_to_string(&mut response).unwrap(); +//! stream.flush().unwrap(); +//! } +//! +//! mock.assert(); +//! ``` +//! +//! You can also work with ranges, by using the `Mock::expect_at_least` and `Mock::expect_at_most` methods: +//! +//! ## Example +//! +//! ```no_run +//! use std::net::TcpStream; +//! use std::io::{Read, Write}; +//! +//! let mut s = mockito::Server::new(); +//! +//! let mock = s.mock("GET", "/hello").expect_at_least(2).expect_at_most(4).create(); +//! +//! for _ in 0..3 { +//! // Place a request +//! let mut stream = TcpStream::connect(s.host_with_port()).unwrap(); +//! stream.write_all("GET /hello HTTP/1.1\r\n\r\n".as_bytes()).unwrap(); +//! let mut response = String::new(); +//! stream.read_to_string(&mut response).unwrap(); +//! stream.flush().unwrap(); +//! } +//! +//! mock.assert(); +//! ``` +//! +//! The errors produced by the `assert` method contain information about the tested mock, but also about the +//! **last unmatched request**, which can be very useful to track down an error in your implementation or +//! a missing or incomplete mock. A colored diff is also displayed. +//! +//! Color output is enabled by default, but can be toggled with the `color` feature flag. +//! +//! Here's an example of how a `Mock#assert` error looks like: +//! +//! ```text +//! > Expected 1 request(s) to: +//! +//! POST /users?number=one +//! bob +//! +//! ...but received 0 +//! +//! > The last unmatched request was: +//! +//! POST /users?number=two +//! content-length: 5 +//! alice +//! +//! > Difference: +//! +//! # A colored diff +//! +//! ``` +//! +//! You can also use the `matched` method to return a boolean for whether the mock was called the +//! correct number of times without panicking +//! +//! ## Example +//! +//! ``` +//! use std::net::TcpStream; +//! use std::io::{Read, Write}; +//! +//! let mut s = mockito::Server::new(); +//! +//! let mock = s.mock("GET", "/").create(); +//! +//! { +//! let mut stream = TcpStream::connect(s.host_with_port()).unwrap(); +//! stream.write_all("GET / HTTP/1.1\r\n\r\n".as_bytes()).unwrap(); +//! let mut response = String::new(); +//! stream.read_to_string(&mut response).unwrap(); +//! stream.flush().unwrap(); +//! } +//! +//! assert!(mock.matched()); +//! +//! { +//! let mut stream = TcpStream::connect(s.host_with_port()).unwrap(); +//! stream.write_all("GET / HTTP/1.1\r\n\r\n".as_bytes()).unwrap(); +//! let mut response = String::new(); +//! stream.read_to_string(&mut response).unwrap(); +//! stream.flush().unwrap(); +//! } +//! assert!(!mock.matched()); +//! ``` //! //! # Non-matching calls //! @@ -524,13 +582,13 @@ //! ## Example //! //! ``` -//! use mockito::{mock, reset}; +//! let mut s = mockito::Server::new(); //! -//! let _m1 = mock("GET", "/1").create(); -//! let _m2 = mock("GET", "/2").create(); -//! let _m3 = mock("GET", "/3").create(); +//! let _m1 = s.mock("GET", "/1").create(); +//! let _m2 = s.mock("GET", "/2").create(); +//! let _m3 = s.mock("GET", "/3").create(); //! -//! reset(); +//! s.reset(); //! //! // Nothing is mocked at this point //! ``` @@ -540,10 +598,10 @@ //! ## Example //! //! ``` -//! use mockito::mock; //! use std::mem; //! -//! let m = mock("GET", "/hello").create(); +//! let mut s = mockito::Server::new(); +//! let m = s.mock("GET", "/hello").create(); //! //! // Requests to GET /hello are mocked //! @@ -557,7 +615,7 @@ //! Mockito uses the `env_logger` crate under the hood to provide useful debugging information. //! //! If you'd like to activate the debug output, introduce the [env_logger](https://crates.rs/crates/env_logger) crate -//! within your project and initialize it before each test that needs debugging: +//! to your project and initialize it before each test that needs debugging: //! //! ``` //! #[test] @@ -573,841 +631,54 @@ //! RUST_LOG=mockito=debug cargo test //! ``` //! -//! # Threads +//! # Sharing the server with other threads //! -//! Mockito records all your mocks on the same server running in the background. For this -//! reason, Mockito tests are run sequentially. This is handled internally via a thread-local -//! mutex lock acquired **whenever you create a mock**. Tests that don't create mocks will -//! still be run in parallel. +//! If you ever need to share the mock server with another thread, make sure to wrap it inside +//! a Mutex or it might not get cleaned properly: //! +//! ## Example +//! +//! ``` +//! use std::sync::{Arc, Mutex}; +//! use std::thread; +//! +//! let mut s = mockito::Server::new(); +//! let host = s.host_with_port(); +//! let _mock_outside_thread = s.mock("GET", "/").with_body("outside").create(); +//! +//! let server_mutex = Arc::new(Mutex::new(s)); +//! let server_clone = server_mutex.clone(); +//! let process = thread::spawn(move || { +//! let mut s = server_clone.lock().unwrap(); +//! let _mock_inside_thread = s.mock("GET", "/").with_body("inside").create(); +//! }); +//! +//! process.join().unwrap(); +//! ``` +//! +pub use error::{Error, ErrorKind}; +use lazy_static::lazy_static; +#[allow(deprecated)] +pub use legacy::{mock, reset, server_address, server_url}; +pub use matcher::Matcher; +pub use mock::Mock; +pub use server::Server; +use tokio::runtime::Runtime; -#[macro_use] -extern crate lazy_static; -#[macro_use] -extern crate log; - +mod command; mod diff; +mod error; +mod legacy; +mod matcher; +mod mock; mod request; mod response; mod server; - -type Request = request::Request; -type Response = response::Response; - -use rand::distributions::Alphanumeric; -use rand::{thread_rng, Rng}; -use regex::Regex; -use std::cell::RefCell; -use std::collections::HashMap; -use std::convert::{From, Into}; -use std::fmt; -use std::fs::File; -use std::io; -use std::io::Read; -use std::ops::Drop; -use std::path::Path; -use std::string::ToString; -use std::sync::Arc; -use std::sync::{LockResult, Mutex, MutexGuard}; +mod server_pool; lazy_static! { - // A global lock that ensure all Mockito tests are run on a single thread. - static ref TEST_MUTEX: Mutex<()> = Mutex::new(()); -} - -thread_local!( - // A thread-local reference to the global lock. This is acquired within `Mock#create()`. - static LOCAL_TEST_MUTEX: RefCell>> = - RefCell::new(TEST_MUTEX.lock()); -); - -/// -/// Points to the address the mock server is running at. -/// Can be used with `std::net::TcpStream`. -/// -#[deprecated(note = "Call server_address() instead")] -pub const SERVER_ADDRESS: &str = SERVER_ADDRESS_INTERNAL; -const SERVER_ADDRESS_INTERNAL: &str = "127.0.0.1:1234"; - -/// -/// Points to the URL the mock server is running at. -/// -#[deprecated(note = "Call server_url() instead")] -pub const SERVER_URL: &str = "http://127.0.0.1:1234"; - -pub use crate::server::address as server_address; -pub use crate::server::url as server_url; -use assert_json_diff::{assert_json_matches_no_panic, CompareMode}; - -/// -/// Initializes a mock for the provided `method` and `path`. -/// -/// The mock is registered to the server only after the `create()` method has been called. -/// -/// ## Example -/// -/// ``` -/// use mockito::mock; -/// -/// let _m1 = mock("GET", "/"); -/// let _m2 = mock("POST", "/users"); -/// let _m3 = mock("DELETE", "/users?id=1"); -/// ``` -/// -pub fn mock>(method: &str, path: P) -> Mock { - Mock::new(method, path) -} - -/// -/// Removes all the mocks stored on the server. -/// -pub fn reset() { - server::try_start(); - - let mut state = server::STATE.lock().unwrap(); - state.mocks.clear(); -} - -#[allow(missing_docs)] -pub fn start() { - server::try_start(); -} - -/// -/// Allows matching the request path or headers in multiple ways: matching the exact value, matching any value (as -/// long as it is present), matching by regular expression or checking that a particular header is missing. -/// -/// These matchers are used within the `mock` and `Mock::match_header` calls. -/// -#[derive(Clone, PartialEq, Debug)] -#[allow(deprecated)] // Rust bug #38832 -pub enum Matcher { - /// Matches the exact path or header value. There's also an implementation of `From<&str>` - /// to keep things simple and backwards compatible. - Exact(String), - /// Matches the body content as a binary file - Binary(BinaryBody), - /// Matches a path or header value by a regular expression. - Regex(String), - /// Matches a specified JSON body from a `serde_json::Value` - Json(serde_json::Value), - /// Matches a specified JSON body from a `String` - JsonString(String), - /// Matches a partial JSON body from a `serde_json::Value` - PartialJson(serde_json::Value), - /// Matches a specified partial JSON body from a `String` - PartialJsonString(String), - /// Matches a URL-encoded key/value pair, where both key and value should be specified - /// in plain (unencoded) format - UrlEncoded(String, String), - /// At least one matcher must match - AnyOf(Vec), - /// All matchers must match - AllOf(Vec), - /// Matches any path or any header value. - Any, - /// Checks that a header is not present in the request. - Missing, -} - -impl<'a> From<&'a str> for Matcher { - fn from(value: &str) -> Self { - Matcher::Exact(value.to_string()) - } -} - -#[allow(clippy::fallible_impl_from)] -impl From<&Path> for Matcher { - fn from(value: &Path) -> Self { - // We want the code to panic if the path is not readable. - Matcher::Binary(BinaryBody::from_path(value).unwrap()) - } -} - -impl From<&mut File> for Matcher { - fn from(value: &mut File) -> Self { - Matcher::Binary(BinaryBody::from_file(value)) - } -} - -impl From> for Matcher { - fn from(value: Vec) -> Self { - Matcher::Binary(BinaryBody::from_bytes(value)) - } -} - -impl fmt::Display for Matcher { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - let join_matches = |matches: &[Self]| { - matches - .iter() - .map(Self::to_string) - .fold(String::new(), |acc, matcher| { - if acc.is_empty() { - matcher - } else { - format!("{}, {}", acc, matcher) - } - }) - }; - - let result = match self { - Matcher::Exact(ref value) => value.to_string(), - Matcher::Binary(ref file) => format!("{} (binary)", file), - Matcher::Regex(ref value) => format!("{} (regex)", value), - Matcher::Json(ref json_obj) => format!("{} (json)", json_obj), - Matcher::JsonString(ref value) => format!("{} (json)", value), - Matcher::PartialJson(ref json_obj) => format!("{} (partial json)", json_obj), - Matcher::PartialJsonString(ref value) => format!("{} (partial json)", value), - Matcher::UrlEncoded(ref field, ref value) => { - format!("{}={} (urlencoded)", field, value) - } - Matcher::Any => "(any)".to_string(), - Matcher::AnyOf(x) => format!("({}) (any of)", join_matches(x)), - Matcher::AllOf(x) => format!("({}) (all of)", join_matches(x)), - Matcher::Missing => "(missing)".to_string(), - }; - write!(f, "{}", result) - } -} - -impl Matcher { - fn matches_values(&self, header_values: &[&str]) -> bool { - match self { - Matcher::Missing => header_values.is_empty(), - // AnyOf([…Missing…]) is handled here, but - // AnyOf([Something]) is handled in the last block. - // That's because Missing matches against all values at once, - // but other matchers match against individual values. - Matcher::AnyOf(ref matchers) if header_values.is_empty() => { - matchers.iter().any(|m| m.matches_values(header_values)) - } - Matcher::AllOf(ref matchers) if header_values.is_empty() => { - matchers.iter().all(|m| m.matches_values(header_values)) - } - _ => { - !header_values.is_empty() && header_values.iter().all(|val| self.matches_value(val)) - } - } - } - - fn matches_binary_value(&self, binary: &[u8]) -> bool { - match self { - Matcher::Binary(ref file) => binary == &*file.content, - _ => false, - } - } - - #[allow(deprecated)] - fn matches_value(&self, other: &str) -> bool { - let compare_json_config = assert_json_diff::Config::new(CompareMode::Inclusive); - match self { - Matcher::Exact(ref value) => value == other, - Matcher::Binary(_) => false, - Matcher::Regex(ref regex) => Regex::new(regex).unwrap().is_match(other), - Matcher::Json(ref json_obj) => { - let other: serde_json::Value = serde_json::from_str(other).unwrap(); - *json_obj == other - } - Matcher::JsonString(ref value) => { - let value: serde_json::Value = serde_json::from_str(value).unwrap(); - let other: serde_json::Value = serde_json::from_str(other).unwrap(); - value == other - } - Matcher::PartialJson(ref json_obj) => { - let actual: serde_json::Value = serde_json::from_str(other).unwrap(); - let expected = json_obj.clone(); - assert_json_matches_no_panic(&actual, &expected, compare_json_config).is_ok() - } - Matcher::PartialJsonString(ref value) => { - let expected: serde_json::Value = serde_json::from_str(value).unwrap(); - let actual: serde_json::Value = serde_json::from_str(other).unwrap(); - assert_json_matches_no_panic(&actual, &expected, compare_json_config).is_ok() - } - Matcher::UrlEncoded(ref expected_field, ref expected_value) => { - serde_urlencoded::from_str::>(other) - .map(|params: HashMap<_, _>| { - params.into_iter().any(|(ref field, ref value)| { - field == expected_field && value == expected_value - }) - }) - .unwrap_or(false) - } - Matcher::Any => true, - Matcher::AnyOf(ref matchers) => matchers.iter().any(|m| m.matches_value(other)), - Matcher::AllOf(ref matchers) => matchers.iter().all(|m| m.matches_value(other)), - Matcher::Missing => other.is_empty(), - } - } -} - -#[derive(Clone, PartialEq, Debug)] -enum PathAndQueryMatcher { - Unified(Matcher), - Split(Box, Box), -} - -impl PathAndQueryMatcher { - fn matches_value(&self, other: &str) -> bool { - match self { - PathAndQueryMatcher::Unified(matcher) => matcher.matches_value(other), - PathAndQueryMatcher::Split(ref path_matcher, ref query_matcher) => { - let mut parts = other.splitn(2, '?'); - let path = parts.next().unwrap(); - let query = parts.next().unwrap_or(""); - - path_matcher.matches_value(path) && query_matcher.matches_value(query) - } - } - } -} - -/// -/// Represents a binary object the body should be matched against -/// -#[derive(Debug, Clone)] -pub struct BinaryBody { - path: Option, - content: Vec, -} - -impl BinaryBody { - /// Read the content from path and initialize a `BinaryBody` - /// - /// # Errors - /// - /// The same resulting from a failed `std::fs::read`. - pub fn from_path(path: &Path) -> Result { - Ok(Self { - path: path.to_str().map(ToString::to_string), - content: std::fs::read(path)?, - }) - } - - /// Read the content from a &mut File and initialize a `BinaryBody` - pub fn from_file(file: &mut File) -> Self { - Self { - path: None, - content: get_content_from(file), - } - } - - /// Instantiate the matcher directly passing the content - #[allow(clippy::missing_const_for_fn)] - pub fn from_bytes(content: Vec) -> Self { - Self { - path: None, - content, - } - } -} - -fn get_content_from(file: &mut File) -> Vec { - let mut filecontent: Vec = Vec::new(); - file.read_to_end(&mut filecontent).unwrap(); - filecontent -} - -impl PartialEq for BinaryBody { - fn eq(&self, other: &Self) -> bool { - match (self.path.as_ref(), other.path.as_ref()) { - (Some(p), Some(o)) => p == o, - _ => self.content == other.content, - } - } -} - -impl fmt::Display for BinaryBody { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - if let Some(filepath) = self.path.as_ref() { - write!(f, "filepath: {}", filepath) - } else { - let len: usize = std::cmp::min(self.content.len(), 8); - let first_bytes: Vec = self.content.iter().copied().take(len).collect(); - write!(f, "filecontent: {:?}", first_bytes) - } - } -} - -/// -/// Stores information about a mocked request. Should be initialized via `mockito::mock()`. -/// -#[derive(Clone, PartialEq, Debug)] -pub struct Mock { - id: String, - method: String, - path: PathAndQueryMatcher, - headers: Vec<(String, Matcher)>, - body: Matcher, - response: Response, - hits: usize, - expected_hits_at_least: Option, - expected_hits_at_most: Option, - is_remote: bool, - - /// Used to warn of mocks missing a `.create()` call. See issue #112 - created: bool, -} - -impl Mock { - fn new>(method: &str, path: P) -> Self { - Self { - id: thread_rng() - .sample_iter(&Alphanumeric) - .map(char::from) - .take(24) - .collect(), - method: method.to_owned().to_uppercase(), - path: PathAndQueryMatcher::Unified(path.into()), - headers: Vec::new(), - body: Matcher::Any, - response: Response::default(), - hits: 0, - expected_hits_at_least: None, - expected_hits_at_most: None, - is_remote: false, - created: false, - } - } - - /// - /// Allows matching against the query part when responding with a mock. - /// - /// Note that you can also specify the query as part of the path argument - /// in a `mock` call, in which case an exact match will be performed. - /// Any future calls of `Mock#match_query` will override the query matcher. - /// - /// ## Example - /// - /// ``` - /// use mockito::{mock, Matcher}; - /// - /// // This will match requests containing the URL-encoded - /// // query parameter `greeting=good%20day` - /// let _m1 = mock("GET", "/test") - /// .match_query(Matcher::UrlEncoded("greeting".into(), "good day".into())) - /// .create(); - /// - /// // This will match requests containing the URL-encoded - /// // query parameters `hello=world` and `greeting=good%20day` - /// let _m2 = mock("GET", "/test") - /// .match_query(Matcher::AllOf(vec![ - /// Matcher::UrlEncoded("hello".into(), "world".into()), - /// Matcher::UrlEncoded("greeting".into(), "good day".into()) - /// ])) - /// .create(); - /// - /// // You can achieve similar results with the regex matcher - /// let _m3 = mock("GET", "/test") - /// .match_query(Matcher::Regex("hello=world".into())) - /// .create(); - /// ``` - /// - pub fn match_query>(mut self, query: M) -> Self { - let new_path = match &self.path { - PathAndQueryMatcher::Unified(matcher) => { - PathAndQueryMatcher::Split(Box::new(matcher.clone()), Box::new(query.into())) - } - PathAndQueryMatcher::Split(path, _) => { - PathAndQueryMatcher::Split(path.clone(), Box::new(query.into())) - } - }; - - self.path = new_path; - - self - } - - /// - /// Allows matching a particular request header when responding with a mock. - /// - /// When matching a request, the field letter case is ignored. - /// - /// ## Example - /// - /// ``` - /// use mockito::mock; - /// - /// let _m = mock("GET", "/").match_header("content-type", "application/json"); - /// ``` - /// - /// Like most other `Mock` methods, it allows chanining: - /// - /// ## Example - /// - /// ``` - /// use mockito::mock; - /// - /// let _m = mock("GET", "/") - /// .match_header("content-type", "application/json") - /// .match_header("authorization", "password"); - /// ``` - /// - pub fn match_header>(mut self, field: &str, value: M) -> Self { - self.headers - .push((field.to_owned().to_lowercase(), value.into())); - - self - } - - /// - /// Allows matching a particular request body when responding with a mock. - /// - /// ## Example - /// - /// ``` - /// use mockito::mock; - /// - /// let _m1 = mock("POST", "/").match_body(r#"{"hello": "world"}"#).with_body("json").create(); - /// let _m2 = mock("POST", "/").match_body("hello=world").with_body("form").create(); - /// - /// // Requests passing `{"hello": "world"}` inside the body will be responded with "json". - /// // Requests passing `hello=world` inside the body will be responded with "form". - /// - /// // Create a temporary file - /// use std::env; - /// use std::fs::File; - /// use std::io::Write; - /// use std::path::Path; - /// use rand; - /// use rand::Rng; - /// - /// let random_bytes: Vec = (0..1024).map(|_| rand::random::()).collect(); - /// - /// let mut tmp_file = env::temp_dir(); - /// tmp_file.push("test_file.txt"); - /// let mut f_write = File::create(tmp_file.clone()).unwrap(); - /// f_write.write_all(random_bytes.as_slice()).unwrap(); - /// let mut f_read = File::open(tmp_file.clone()).unwrap(); - /// - /// - /// // the following are equivalent ways of defining a mock matching - /// // a binary payload - /// let _b1 = mock("POST", "/").match_body(tmp_file.as_path()).create(); - /// let _b3 = mock("POST", "/").match_body(random_bytes).create(); - /// let _b2 = mock("POST", "/").match_body(&mut f_read).create(); - /// ``` - /// - pub fn match_body>(mut self, body: M) -> Self { - self.body = body.into(); - - self - } - - /// - /// Sets the status code of the mock response. The default status code is 200. - /// - /// ## Example - /// - /// ``` - /// use mockito::mock; - /// - /// let _m = mock("GET", "/").with_status(201); - /// ``` - /// - pub fn with_status(mut self, status: usize) -> Self { - self.response.status = status.into(); - - self - } - - /// - /// Sets a header of the mock response. - /// - /// ## Example - /// - /// ``` - /// use mockito::mock; - /// - /// let _m = mock("GET", "/").with_header("content-type", "application/json"); - /// ``` - /// - pub fn with_header(mut self, field: &str, value: &str) -> Self { - self.response - .headers - .push((field.to_owned(), value.to_owned())); - - self - } - - /// - /// Sets the body of the mock response. Its `Content-Length` is handled automatically. - /// - /// ## Example - /// - /// ``` - /// use mockito::mock; - /// - /// let _m = mock("GET", "/").with_body("hello world"); - /// ``` - /// - pub fn with_body>(mut self, body: StrOrBytes) -> Self { - self.response.body = response::Body::Bytes(body.as_ref().to_owned()); - self - } - - /// - /// Sets the body of the mock response dynamically. The response will use chunked transfer encoding. - /// - /// 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. - /// - /// ## Example - /// - /// ``` - /// use mockito::mock; - /// - /// let _m = mock("GET", "/").with_body_from_fn(|w| w.write_all(b"hello world")); - /// ``` - /// - pub fn with_body_from_fn( - mut self, - cb: impl Fn(&mut dyn io::Write) -> io::Result<()> + Send + Sync + 'static, - ) -> Self { - self.response.body = response::Body::Fn(Arc::new(cb)); - self - } - - /// - /// Sets the body of the mock response from the contents of a file stored under `path`. - /// Its `Content-Length` is handled automatically. - /// - /// ## Example - /// - /// ``` - /// use mockito::mock; - /// - /// let _m = mock("GET", "/").with_body_from_file("tests/files/simple.http"); - /// ``` - /// - pub fn with_body_from_file(mut self, path: impl AsRef) -> Self { - self.response.body = response::Body::Bytes(std::fs::read(path).unwrap()); - self - } - - /// - /// Sets the expected amount of requests that this mock is supposed to receive. - /// This is only enforced when calling the `assert` method. - /// Defaults to 1 request. - /// - #[allow(clippy::missing_const_for_fn)] - pub fn expect(mut self, hits: usize) -> Self { - self.expected_hits_at_least = Some(hits); - self.expected_hits_at_most = Some(hits); - self - } - - /// - /// Sets the minimum amount of requests that this mock is supposed to receive. - /// This is only enforced when calling the `assert` method. - /// - pub fn expect_at_least(mut self, hits: usize) -> Self { - self.expected_hits_at_least = Some(hits); - if self.expected_hits_at_most.is_some() - && self.expected_hits_at_most < self.expected_hits_at_least - { - self.expected_hits_at_most = None; - } - self - } - - /// - /// Sets the maximum amount of requests that this mock is supposed to receive. - /// This is only enforced when calling the `assert` method. - /// - pub fn expect_at_most(mut self, hits: usize) -> Self { - self.expected_hits_at_most = Some(hits); - if self.expected_hits_at_least.is_some() - && self.expected_hits_at_least > self.expected_hits_at_most - { - self.expected_hits_at_least = None; - } - self - } - - /// - /// Asserts that the expected amount of requests (defaults to 1 request) were performed. - /// - pub fn assert(&self) { - let mut opt_message = None; - - { - let state = server::STATE.lock().unwrap(); - - if let Some(remote_mock) = state.mocks.iter().find(|mock| mock.id == self.id) { - let mut message = match (self.expected_hits_at_least, self.expected_hits_at_most) { - (Some(min), Some(max)) if min == max => format!( - "\n> Expected {} request(s) to:\n{}\n...but received {}\n\n", - min, self, remote_mock.hits - ), - (Some(min), Some(max)) => format!( - "\n> Expected between {} and {} request(s) to:\n{}\n...but received {}\n\n", - min, max, self, remote_mock.hits - ), - (Some(min), None) => format!( - "\n> Expected at least {} request(s) to:\n{}\n...but received {}\n\n", - min, self, remote_mock.hits - ), - (None, Some(max)) => format!( - "\n> Expected at most {} request(s) to:\n{}\n...but received {}\n\n", - max, self, remote_mock.hits - ), - (None, None) => format!( - "\n> Expected 1 request(s) to:\n{}\n...but received {}\n\n", - self, remote_mock.hits - ), - }; - - if let Some(last_request) = state.unmatched_requests.last() { - message.push_str(&format!( - "> The last unmatched request was:\n{}\n", - last_request - )); - - let difference = diff::compare(&self.to_string(), &last_request.to_string()); - message.push_str(&format!("> Difference:\n{}\n", difference)); - } - - opt_message = Some(message); - } - } - - if let Some(message) = opt_message { - assert!(self.matched(), "{}", message) - } else { - panic!("Could not retrieve enough information about the remote mock.") - } - } - - /// - /// Returns whether the expected amount of requests (defaults to 1) were performed. - /// - pub fn matched(&self) -> bool { - let state = server::STATE.lock().unwrap(); - - state - .mocks - .iter() - .find(|mock| mock.id == self.id) - .map_or(false, |remote_mock| { - let hits = remote_mock.hits; - - match (self.expected_hits_at_least, self.expected_hits_at_most) { - (Some(min), Some(max)) => hits >= min && hits <= max, - (Some(min), None) => hits >= min, - (None, Some(max)) => hits <= max, - (None, None) => hits == 1, - } - }) - } - - /// - /// Registers the mock to the server - your mock will be served only after calling this method. - /// - /// ## Example - /// - /// ``` - /// use mockito::mock; - /// - /// let _m = mock("GET", "/").with_body("hello world").create(); - /// ``` - /// - #[must_use] - pub fn create(mut self) -> Self { - server::try_start(); - - // Ensures Mockito tests are run sequentially. - LOCAL_TEST_MUTEX.with(|_| {}); - - let mut state = server::STATE.lock().unwrap(); - - self.created = true; - - let mut remote_mock = self.clone(); - remote_mock.is_remote = true; - state.mocks.push(remote_mock); - - self - } - - #[allow(clippy::missing_const_for_fn)] - fn is_local(&self) -> bool { - !self.is_remote - } -} - -impl Drop for Mock { - fn drop(&mut self) { - if self.is_local() { - let mut state = server::STATE.lock().unwrap(); - - if let Some(pos) = state.mocks.iter().position(|mock| mock.id == self.id) { - state.mocks.remove(pos); - } - - debug!("Mock::drop() called for {}", self); - - if !self.created { - warn!("Missing .create() call on mock {}", self); - } - } - } -} - -impl fmt::Display for PathAndQueryMatcher { - #[allow(deprecated)] - #[allow(clippy::write_with_newline)] - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - match self { - PathAndQueryMatcher::Unified(matcher) => write!(f, "{}\r\n", &matcher), - PathAndQueryMatcher::Split(path, query) => write!(f, "{}?{}\r\n", &path, &query), - } - } -} - -impl fmt::Display for Mock { - #[allow(deprecated)] - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - let mut formatted = String::new(); - - formatted.push_str("\r\n"); - formatted.push_str(&self.method); - formatted.push(' '); - formatted.push_str(&self.path.to_string()); - - for &(ref key, ref value) in &self.headers { - formatted.push_str(key); - formatted.push_str(": "); - formatted.push_str(&value.to_string()); - formatted.push_str("\r\n"); - } - - match self.body { - Matcher::Exact(ref value) - | Matcher::JsonString(ref value) - | Matcher::PartialJsonString(ref value) - | Matcher::Regex(ref value) => { - formatted.push_str(value); - formatted.push_str("\r\n"); - } - Matcher::Binary(_) => { - formatted.push_str("(binary)\r\n"); - } - Matcher::Json(ref json_obj) | Matcher::PartialJson(ref json_obj) => { - formatted.push_str(&json_obj.to_string()); - formatted.push_str("\r\n") - } - Matcher::UrlEncoded(ref field, ref value) => { - formatted.push_str(field); - formatted.push('='); - formatted.push_str(value); - } - Matcher::Missing => formatted.push_str("(missing)\r\n"), - Matcher::AnyOf(..) => formatted.push_str("(any of)\r\n"), - Matcher::AllOf(..) => formatted.push_str("(all of)\r\n"), - Matcher::Any => {} - } - - f.write_str(&formatted) - } + pub(crate) static ref RUNTIME: Runtime = tokio::runtime::Builder::new_current_thread() + .enable_all() + .build() + .expect("couldn't start tokio runtime"); } diff --git a/src/matcher.rs b/src/matcher.rs new file mode 100644 index 0000000..1b4b65a --- /dev/null +++ b/src/matcher.rs @@ -0,0 +1,277 @@ +use assert_json_diff::{assert_json_matches_no_panic, CompareMode}; +use regex::Regex; +use std::collections::HashMap; +use std::convert::From; +use std::fmt; +use std::fs::File; +use std::io; +use std::io::Read; +use std::path::Path; +use std::string::ToString; + +/// +/// Allows matching the request path, headers or body in multiple ways: by the exact value, by any value (as +/// long as it is present), by regular expression or by checking that a particular header is missing. +/// +/// These matchers can be used within the `Server::mock`, `Mock::match_header` or `Mock::match_body` calls. +/// +#[derive(Clone, PartialEq, Debug)] +#[allow(deprecated)] // Rust bug #38832 +pub enum Matcher { + /// Matches the exact path or header value. There's also an implementation of `From<&str>` + /// to keep things simple and backwards compatible. + Exact(String), + /// Matches the body content as a binary file + Binary(BinaryBody), + /// Matches a path or header value by a regular expression. + Regex(String), + /// Matches a specified JSON body from a `serde_json::Value` + Json(serde_json::Value), + /// Matches a specified JSON body from a `String` + JsonString(String), + /// Matches a partial JSON body from a `serde_json::Value` + PartialJson(serde_json::Value), + /// Matches a specified partial JSON body from a `String` + PartialJsonString(String), + /// Matches a URL-encoded key/value pair, where both key and value should be specified + /// in plain (unencoded) format + UrlEncoded(String, String), + /// At least one matcher must match + AnyOf(Vec), + /// All matchers must match + AllOf(Vec), + /// Matches any path or any header value. + Any, + /// Checks that a header is not present in the request. + Missing, +} + +impl<'a> From<&'a str> for Matcher { + fn from(value: &str) -> Self { + Matcher::Exact(value.to_string()) + } +} + +#[allow(clippy::fallible_impl_from)] +impl From<&Path> for Matcher { + fn from(value: &Path) -> Self { + // We want the code to panic if the path is not readable. + Matcher::Binary(BinaryBody::from_path(value).unwrap()) + } +} + +impl From<&mut File> for Matcher { + fn from(value: &mut File) -> Self { + Matcher::Binary(BinaryBody::from_file(value)) + } +} + +impl From> for Matcher { + fn from(value: Vec) -> Self { + Matcher::Binary(BinaryBody::from_bytes(value)) + } +} + +impl fmt::Display for Matcher { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let join_matches = |matches: &[Self]| { + matches + .iter() + .map(Self::to_string) + .fold(String::new(), |acc, matcher| { + if acc.is_empty() { + matcher + } else { + format!("{}, {}", acc, matcher) + } + }) + }; + + let result = match self { + Matcher::Exact(ref value) => value.to_string(), + Matcher::Binary(ref file) => format!("{} (binary)", file), + Matcher::Regex(ref value) => format!("{} (regex)", value), + Matcher::Json(ref json_obj) => format!("{} (json)", json_obj), + Matcher::JsonString(ref value) => format!("{} (json)", value), + Matcher::PartialJson(ref json_obj) => format!("{} (partial json)", json_obj), + Matcher::PartialJsonString(ref value) => format!("{} (partial json)", value), + Matcher::UrlEncoded(ref field, ref value) => { + format!("{}={} (urlencoded)", field, value) + } + Matcher::Any => "(any)".to_string(), + Matcher::AnyOf(x) => format!("({}) (any of)", join_matches(x)), + Matcher::AllOf(x) => format!("({}) (all of)", join_matches(x)), + Matcher::Missing => "(missing)".to_string(), + }; + write!(f, "{}", result) + } +} + +impl Matcher { + pub(crate) fn matches_values(&self, header_values: &[&str]) -> bool { + match self { + Matcher::Missing => header_values.is_empty(), + // AnyOf([…Missing…]) is handled here, but + // AnyOf([Something]) is handled in the last block. + // That's because Missing matches against all values at once, + // but other matchers match against individual values. + Matcher::AnyOf(ref matchers) if header_values.is_empty() => { + matchers.iter().any(|m| m.matches_values(header_values)) + } + Matcher::AllOf(ref matchers) if header_values.is_empty() => { + matchers.iter().all(|m| m.matches_values(header_values)) + } + _ => { + !header_values.is_empty() && header_values.iter().all(|val| self.matches_value(val)) + } + } + } + + pub(crate) fn matches_binary_value(&self, binary: &[u8]) -> bool { + match self { + Matcher::Binary(ref file) => binary == &*file.content, + _ => false, + } + } + + #[allow(deprecated)] + pub(crate) fn matches_value(&self, other: &str) -> bool { + let compare_json_config = assert_json_diff::Config::new(CompareMode::Inclusive); + match self { + Matcher::Exact(ref value) => value == other, + Matcher::Binary(_) => false, + Matcher::Regex(ref regex) => Regex::new(regex).unwrap().is_match(other), + Matcher::Json(ref json_obj) => { + let other: serde_json::Value = serde_json::from_str(other).unwrap(); + *json_obj == other + } + Matcher::JsonString(ref value) => { + let value: serde_json::Value = serde_json::from_str(value).unwrap(); + let other: serde_json::Value = serde_json::from_str(other).unwrap(); + value == other + } + Matcher::PartialJson(ref json_obj) => { + let actual: serde_json::Value = serde_json::from_str(other).unwrap(); + let expected = json_obj.clone(); + assert_json_matches_no_panic(&actual, &expected, compare_json_config).is_ok() + } + Matcher::PartialJsonString(ref value) => { + let expected: serde_json::Value = serde_json::from_str(value).unwrap(); + let actual: serde_json::Value = serde_json::from_str(other).unwrap(); + assert_json_matches_no_panic(&actual, &expected, compare_json_config).is_ok() + } + Matcher::UrlEncoded(ref expected_field, ref expected_value) => { + serde_urlencoded::from_str::>(other) + .map(|params: HashMap<_, _>| { + params.into_iter().any(|(ref field, ref value)| { + field == expected_field && value == expected_value + }) + }) + .unwrap_or(false) + } + Matcher::Any => true, + Matcher::AnyOf(ref matchers) => matchers.iter().any(|m| m.matches_value(other)), + Matcher::AllOf(ref matchers) => matchers.iter().all(|m| m.matches_value(other)), + Matcher::Missing => other.is_empty(), + } + } +} + +#[derive(Clone, PartialEq, Debug)] +pub(crate) enum PathAndQueryMatcher { + Unified(Matcher), + Split(Box, Box), +} + +impl PathAndQueryMatcher { + pub(crate) fn matches_value(&self, other: &str) -> bool { + match self { + PathAndQueryMatcher::Unified(matcher) => matcher.matches_value(other), + PathAndQueryMatcher::Split(ref path_matcher, ref query_matcher) => { + let mut parts = other.splitn(2, '?'); + let path = parts.next().unwrap(); + let query = parts.next().unwrap_or(""); + + path_matcher.matches_value(path) && query_matcher.matches_value(query) + } + } + } +} + +impl fmt::Display for PathAndQueryMatcher { + #[allow(deprecated)] + #[allow(clippy::write_with_newline)] + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + PathAndQueryMatcher::Unified(matcher) => write!(f, "{}\r\n", &matcher), + PathAndQueryMatcher::Split(path, query) => write!(f, "{}?{}\r\n", &path, &query), + } + } +} + +/// +/// Represents a binary object the body should be matched against +/// +#[derive(Debug, Clone)] +pub struct BinaryBody { + path: Option, + content: Vec, +} + +impl BinaryBody { + /// Read the content from path and initialize a `BinaryBody` + /// + /// # Errors + /// + /// The same resulting from a failed `std::fs::read`. + pub fn from_path(path: &Path) -> Result { + Ok(Self { + path: path.to_str().map(ToString::to_string), + content: std::fs::read(path)?, + }) + } + + /// Read the content from a &mut File and initialize a `BinaryBody` + pub fn from_file(file: &mut File) -> Self { + Self { + path: None, + content: get_content_from(file), + } + } + + /// Instantiate the matcher directly passing the content + #[allow(clippy::missing_const_for_fn)] + pub fn from_bytes(content: Vec) -> Self { + Self { + path: None, + content, + } + } +} + +fn get_content_from(file: &mut File) -> Vec { + let mut filecontent: Vec = Vec::new(); + file.read_to_end(&mut filecontent).unwrap(); + filecontent +} + +impl PartialEq for BinaryBody { + fn eq(&self, other: &Self) -> bool { + match (self.path.as_ref(), other.path.as_ref()) { + (Some(p), Some(o)) => p == o, + _ => self.content == other.content, + } + } +} + +impl fmt::Display for BinaryBody { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + if let Some(filepath) = self.path.as_ref() { + write!(f, "filepath: {}", filepath) + } else { + let len: usize = std::cmp::min(self.content.len(), 8); + let first_bytes: Vec = self.content.iter().copied().take(len).collect(); + write!(f, "filecontent: {:?}", first_bytes) + } + } +} diff --git a/src/mock.rs b/src/mock.rs new file mode 100644 index 0000000..fb262b7 --- /dev/null +++ b/src/mock.rs @@ -0,0 +1,544 @@ +use crate::command::Command; +use crate::diff; +use crate::matcher::{Matcher, PathAndQueryMatcher}; +use crate::response::{Body, Response}; +use crate::server::RemoteMock; +use crate::{Error, ErrorKind}; +use hyper::StatusCode; +use rand::distributions::Alphanumeric; +use rand::{thread_rng, Rng}; +use std::convert::Into; +use std::fmt; +use std::io; +use std::ops::Drop; +use std::path::Path; +use std::string::ToString; +use std::sync::Arc; +use tokio::sync::mpsc::Sender; + +#[derive(Clone, Debug)] +pub struct InnerMock { + pub(crate) id: String, + pub(crate) method: String, + pub(crate) path: PathAndQueryMatcher, + pub(crate) headers: Vec<(String, Matcher)>, + pub(crate) body: Matcher, + pub(crate) response: Response, + pub(crate) hits: usize, + pub(crate) expected_hits_at_least: Option, + pub(crate) expected_hits_at_most: Option, +} + +impl fmt::Display for InnerMock { + #[allow(deprecated)] + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + let mut formatted = String::new(); + + formatted.push_str("\r\n"); + formatted.push_str(&self.method); + formatted.push(' '); + formatted.push_str(&self.path.to_string()); + + for &(ref key, ref value) in &self.headers { + formatted.push_str(key); + formatted.push_str(": "); + formatted.push_str(&value.to_string()); + formatted.push_str("\r\n"); + } + + match self.body { + Matcher::Exact(ref value) + | Matcher::JsonString(ref value) + | Matcher::PartialJsonString(ref value) + | Matcher::Regex(ref value) => { + formatted.push_str(value); + formatted.push_str("\r\n"); + } + Matcher::Binary(_) => { + formatted.push_str("(binary)\r\n"); + } + Matcher::Json(ref json_obj) | Matcher::PartialJson(ref json_obj) => { + formatted.push_str(&json_obj.to_string()); + formatted.push_str("\r\n") + } + Matcher::UrlEncoded(ref field, ref value) => { + formatted.push_str(field); + formatted.push('='); + formatted.push_str(value); + } + Matcher::Missing => formatted.push_str("(missing)\r\n"), + Matcher::AnyOf(..) => formatted.push_str("(any of)\r\n"), + Matcher::AllOf(..) => formatted.push_str("(all of)\r\n"), + Matcher::Any => {} + } + + f.write_str(&formatted) + } +} + +impl PartialEq for InnerMock { + fn eq(&self, other: &Self) -> bool { + self.id == other.id + && self.method == other.method + && self.path == other.path + && self.headers == other.headers + && self.body == other.body + && self.response == other.response + && self.hits == other.hits + } +} + +/// +/// Stores information about a mocked request. Should be initialized via `Server::mock()`. +/// +#[derive(Debug)] +pub struct Mock { + sender: Sender, + inner: InnerMock, + /// Used to warn of mocks missing a `.create()` call. See issue #112 + created: bool, +} + +impl Mock { + pub(crate) fn new>(sender: Sender, method: &str, path: P) -> Mock { + let inner = InnerMock { + id: thread_rng() + .sample_iter(&Alphanumeric) + .map(char::from) + .take(24) + .collect(), + method: method.to_owned().to_uppercase(), + path: PathAndQueryMatcher::Unified(path.into()), + headers: Vec::new(), + body: Matcher::Any, + response: Response::default(), + hits: 0, + expected_hits_at_least: None, + expected_hits_at_most: None, + }; + + Self { + sender, + inner, + created: false, + } + } + + /// + /// Allows matching against the query part when responding with a mock. + /// + /// Note that you can also specify the query as part of the path argument + /// in a `mock` call, in which case an exact match will be performed. + /// Any future calls of `Mock#match_query` will override the query matcher. + /// + /// ## Example + /// + /// ``` + /// use mockito::Matcher; + /// + /// let mut s = mockito::Server::new(); + /// + /// // This will match requests containing the URL-encoded + /// // query parameter `greeting=good%20day` + /// let _m1 = s.mock("GET", "/test") + /// .match_query(Matcher::UrlEncoded("greeting".into(), "good day".into())) + /// .create(); + /// + /// // This will match requests containing the URL-encoded + /// // query parameters `hello=world` and `greeting=good%20day` + /// let _m2 = s.mock("GET", "/test") + /// .match_query(Matcher::AllOf(vec![ + /// Matcher::UrlEncoded("hello".into(), "world".into()), + /// Matcher::UrlEncoded("greeting".into(), "good day".into()) + /// ])) + /// .create(); + /// + /// // You can achieve similar results with the regex matcher + /// let _m3 = s.mock("GET", "/test") + /// .match_query(Matcher::Regex("hello=world".into())) + /// .create(); + /// ``` + /// + pub fn match_query>(mut self, query: M) -> Self { + let new_path = match &self.inner.path { + PathAndQueryMatcher::Unified(matcher) => { + PathAndQueryMatcher::Split(Box::new(matcher.clone()), Box::new(query.into())) + } + PathAndQueryMatcher::Split(path, _) => { + PathAndQueryMatcher::Split(path.clone(), Box::new(query.into())) + } + }; + + self.inner.path = new_path; + + self + } + + /// + /// Allows matching a particular request header when responding with a mock. + /// + /// When matching a request, the field letter case is ignored. + /// + /// ## Example + /// + /// ``` + /// let mut s = mockito::Server::new(); + /// + /// let _m = s.mock("GET", "/").match_header("content-type", "application/json"); + /// ``` + /// + /// Like most other `Mock` methods, it allows chanining: + /// + /// ## Example + /// + /// ``` + /// let mut s = mockito::Server::new(); + /// + /// let _m = s.mock("GET", "/") + /// .match_header("content-type", "application/json") + /// .match_header("authorization", "password"); + /// ``` + /// + pub fn match_header>(mut self, field: &str, value: M) -> Self { + self.inner + .headers + .push((field.to_owned().to_lowercase(), value.into())); + + self + } + + /// + /// Allows matching a particular request body when responding with a mock. + /// + /// ## Example + /// + /// ``` + /// let mut s = mockito::Server::new(); + /// + /// let _m1 = s.mock("POST", "/").match_body(r#"{"hello": "world"}"#).with_body("json").create(); + /// let _m2 = s.mock("POST", "/").match_body("hello=world").with_body("form").create(); + /// + /// // Requests passing `{"hello": "world"}` inside the body will be responded with "json". + /// // Requests passing `hello=world` inside the body will be responded with "form". + /// + /// // Create a temporary file + /// use std::env; + /// use std::fs::File; + /// use std::io::Write; + /// use std::path::Path; + /// use rand; + /// use rand::Rng; + /// + /// let random_bytes: Vec = (0..1024).map(|_| rand::random::()).collect(); + /// + /// let mut tmp_file = env::temp_dir(); + /// tmp_file.push("test_file.txt"); + /// let mut f_write = File::create(tmp_file.clone()).unwrap(); + /// f_write.write_all(random_bytes.as_slice()).unwrap(); + /// let mut f_read = File::open(tmp_file.clone()).unwrap(); + /// + /// + /// // the following are equivalent ways of defining a mock matching + /// // a binary payload + /// let _b1 = s.mock("POST", "/").match_body(tmp_file.as_path()).create(); + /// let _b3 = s.mock("POST", "/").match_body(random_bytes).create(); + /// let _b2 = s.mock("POST", "/").match_body(&mut f_read).create(); + /// ``` + /// + pub fn match_body>(mut self, body: M) -> Self { + self.inner.body = body.into(); + + self + } + + /// + /// Sets the status code of the mock response. The default status code is 200. + /// + /// ## Example + /// + /// ``` + /// let mut s = mockito::Server::new(); + /// + /// let _m = s.mock("GET", "/").with_status(201); + /// ``` + /// + #[track_caller] + pub fn with_status(mut self, status: usize) -> Self { + self.inner.response.status = StatusCode::from_u16(status as u16) + .map_err(|_| Error::new_with_context(ErrorKind::InvalidStatusCode, status)) + .unwrap(); + + self + } + + /// + /// Sets a header of the mock response. + /// + /// ## Example + /// + /// ``` + /// let mut s = mockito::Server::new(); + /// + /// let _m = s.mock("GET", "/").with_header("content-type", "application/json"); + /// ``` + /// + pub fn with_header(mut self, field: &str, value: &str) -> Self { + self.inner + .response + .headers + .push((field.to_owned(), value.to_owned())); + + self + } + + /// + /// Sets the body of the mock response. Its `Content-Length` is handled automatically. + /// + /// ## Example + /// + /// ``` + /// let mut s = mockito::Server::new(); + /// + /// let _m = s.mock("GET", "/").with_body("hello world"); + /// ``` + /// + pub fn with_body>(mut self, body: StrOrBytes) -> Self { + self.inner.response.body = Body::Bytes(body.as_ref().to_owned()); + self + } + + /// + /// Sets the body of the mock response dynamically. The response will use chunked transfer encoding. + /// + /// 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. + /// + /// ## Example + /// + /// ``` + /// let mut s = mockito::Server::new(); + /// + /// let _m = s.mock("GET", "/").with_body_from_fn(|w| w.write_all(b"hello world")); + /// ``` + /// + pub fn with_body_from_fn( + mut self, + cb: impl Fn(&mut dyn io::Write) -> io::Result<()> + Send + Sync + 'static, + ) -> Self { + self.inner.response.body = Body::Fn(Arc::new(cb)); + self + } + + /// + /// Sets the body of the mock response from the contents of a file stored under `path`. + /// Its `Content-Length` is handled automatically. + /// + /// ## Example + /// + /// ``` + /// let mut s = mockito::Server::new(); + /// + /// let _m = s.mock("GET", "/").with_body_from_file("tests/files/simple.http"); + /// ``` + /// + #[track_caller] + pub fn with_body_from_file(mut self, path: impl AsRef) -> Self { + self.inner.response.body = Body::Bytes( + std::fs::read(path) + .map_err(|_| Error::new(ErrorKind::FileNotFound)) + .unwrap(), + ); + self + } + + /// + /// Sets the expected amount of requests that this mock is supposed to receive. + /// This is only enforced when calling the `assert` method. + /// Defaults to 1 request. + /// + #[allow(clippy::missing_const_for_fn)] + pub fn expect(mut self, hits: usize) -> Self { + self.inner.expected_hits_at_least = Some(hits); + self.inner.expected_hits_at_most = Some(hits); + self + } + + /// + /// Sets the minimum amount of requests that this mock is supposed to receive. + /// This is only enforced when calling the `assert` method. + /// + pub fn expect_at_least(mut self, hits: usize) -> Self { + self.inner.expected_hits_at_least = Some(hits); + if self.inner.expected_hits_at_most.is_some() + && self.inner.expected_hits_at_most < self.inner.expected_hits_at_least + { + self.inner.expected_hits_at_most = None; + } + self + } + + /// + /// Sets the maximum amount of requests that this mock is supposed to receive. + /// This is only enforced when calling the `assert` method. + /// + pub fn expect_at_most(mut self, hits: usize) -> Self { + self.inner.expected_hits_at_most = Some(hits); + if self.inner.expected_hits_at_least.is_some() + && self.inner.expected_hits_at_least > self.inner.expected_hits_at_most + { + self.inner.expected_hits_at_least = None; + } + self + } + + /// + /// Asserts that the expected amount of requests (defaults to 1 request) were performed. + /// + pub fn assert(&self) { + crate::RUNTIME.block_on(async { self.assert_async().await }) + } + + /// + /// Same as `Mock::assert` but async. + /// + pub async fn assert_async(&self) { + let mut opt_message = None; + + { + let hits = Command::get_mock_hits(&self.sender, self.inner.id.clone()).await; + + if let Some(hits) = hits { + let mut message = match ( + self.inner.expected_hits_at_least, + self.inner.expected_hits_at_most, + ) { + (Some(min), Some(max)) if min == max => format!( + "\n> Expected {} request(s) to:\n{}\n...but received {}\n\n", + min, self, hits + ), + (Some(min), Some(max)) => format!( + "\n> Expected between {} and {} request(s) to:\n{}\n...but received {}\n\n", + min, max, self, hits + ), + (Some(min), None) => format!( + "\n> Expected at least {} request(s) to:\n{}\n...but received {}\n\n", + min, self, hits + ), + (None, Some(max)) => format!( + "\n> Expected at most {} request(s) to:\n{}\n...but received {}\n\n", + max, self, hits + ), + (None, None) => format!( + "\n> Expected 1 request(s) to:\n{}\n...but received {}\n\n", + self, hits + ), + }; + + if let Some(last_request) = Command::get_last_unmatched_request(&self.sender).await + { + message.push_str(&format!( + "> The last unmatched request was:\n{}\n", + last_request + )); + + let difference = diff::compare(&self.to_string(), &last_request); + message.push_str(&format!("> Difference:\n{}\n", difference)); + } + + opt_message = Some(message); + } + } + + if let Some(message) = opt_message { + assert!(self.matched_async().await, "{}", message) + } else { + panic!("Could not retrieve enough information about the remote mock.") + } + } + + /// + /// Returns whether the expected amount of requests (defaults to 1) were performed. + /// + pub fn matched(&self) -> bool { + crate::RUNTIME.block_on(async { self.matched_async().await }) + } + + /// + /// Same as `Mock::matched` but async. + /// + pub async fn matched_async(&self) -> bool { + let Some(hits) = Command::get_mock_hits(&self.sender, self.inner.id.clone()).await else { + return false; + }; + + match ( + self.inner.expected_hits_at_least, + self.inner.expected_hits_at_most, + ) { + (Some(min), Some(max)) => hits >= min && hits <= max, + (Some(min), None) => hits >= min, + (None, Some(max)) => hits <= max, + (None, None) => hits == 1, + } + } + + /// + /// Registers the mock to the server - your mock will be served only after calling this method. + /// + /// ## Example + /// + /// ``` + /// let mut s = mockito::Server::new(); + /// + /// let _m = s.mock("GET", "/").with_body("hello world").create(); + /// ``` + /// + #[must_use] + pub fn create(self) -> Mock { + crate::RUNTIME.block_on(async { self.create_async().await }) + } + + /// + /// Same as `Mock::create` but async. + /// + pub async fn create_async(mut self) -> Mock { + let remote_mock = RemoteMock::new(self.inner.clone()); + let created = Command::create_mock(&self.sender, remote_mock).await; + + self.created = created; + + self + } +} + +impl Drop for Mock { + fn drop(&mut self) { + let sender = self.sender.clone(); + let id = self.inner.id.clone(); + + futures::executor::block_on(async move { + Command::remove_mock(&sender, id).await; + }); + + log::debug!("Mock::drop() called for {}", self); + + if !self.created { + log::warn!("Missing .create() call on mock {}", self); + } + } +} + +impl fmt::Display for Mock { + #[allow(deprecated)] + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + let mut formatted = String::new(); + formatted.push_str(&self.inner.to_string()); + f.write_str(&formatted) + } +} + +impl PartialEq for Mock { + fn eq(&self, other: &Self) -> bool { + self.inner == other.inner + } +} diff --git a/src/request.rs b/src/request.rs index 61619b7..2f001b7 100644 --- a/src/request.rs +++ b/src/request.rs @@ -1,223 +1,84 @@ -use std::convert::From; -use std::default::Default; -use std::fmt; -use std::io::{self, BufRead, BufReader, Cursor, Read}; -use std::mem; -use std::net::TcpStream; -use std::str; +use crate::{Error, ErrorKind}; +use hyper::body; +use hyper::body::Buf; +use hyper::Body as HyperBody; +use hyper::Request as HyperRequest; #[derive(Debug)] -pub struct Request { - pub version: (u8, u8), - pub method: String, - pub path: String, - pub headers: Vec<(String, String)>, - pub body: Vec, - error: Option, - is_parsed: bool, - last_header_field: Option, - last_header_value: Option, +pub(crate) struct Request { + inner: HyperRequest, + body: Option>, } impl Request { - pub fn is_head(&self) -> bool { - self.method == "HEAD" - } - - #[allow(clippy::missing_const_for_fn)] - pub fn is_ok(&self) -> bool { - self.error.is_none() + pub fn new(request: HyperRequest) -> Self { + Request { + inner: request, + body: None, + } } - #[allow(clippy::missing_const_for_fn)] - pub fn is_err(&self) -> bool { - self.error.is_some() + pub fn method(&self) -> &str { + self.inner.method().as_ref() } - #[allow(clippy::missing_const_for_fn)] - pub fn error(&self) -> Option<&String> { - self.error.as_ref() + pub fn path_and_query(&self) -> &str { + self.inner + .uri() + .path_and_query() + .map(|pq| pq.as_str()) + .unwrap_or("") } - pub fn find_header_values(&self, searched_field: &str) -> Vec<&str> { - self.headers + pub fn header(&self, field: &str) -> Vec<&str> { + self.inner + .headers() + .get_all(field) .iter() - .filter_map(|(field, value)| { - if field == searched_field { - Some(value.as_str()) - } else { - None - } - }) - .collect() + .map(|item| item.to_str().unwrap()) + .collect::>() } - fn record_last_header(&mut self) { - if self.last_header_field.is_some() && self.last_header_value.is_some() { - let last_header_field = mem::replace(&mut self.last_header_field, None).unwrap(); - let last_header_value = mem::replace(&mut self.last_header_value, None).unwrap(); - self.headers - .push((last_header_field.to_lowercase(), last_header_value)); - } + pub fn has_header(&self, header_name: &str) -> bool { + self.inner.headers().contains_key(header_name) } - fn content_length(&self) -> Option { - use std::str::FromStr; - - self.find_header_values("content-length") - .first() - .and_then(|len| usize::from_str(*len).ok()) - } - - fn has_chunked_body(&self) -> bool { - self.headers - .iter() - .filter(|(name, _)| name == "transfer-encoding") - .any(|(_, value)| value.contains("chunked")) - } - - fn read_request_body(&mut self, mut raw_body_rd: impl Read) -> io::Result<()> { - if let Some(content_length) = self.content_length() { - let mut rd = raw_body_rd.take(content_length as u64); - return io::copy(&mut rd, &mut self.body).map(|_| ()); - } - - if self.has_chunked_body() { - let mut chunk_size_buf = String::new(); - let mut reader = BufReader::new(raw_body_rd); - - loop { - reader.read_line(&mut chunk_size_buf)?; - - let chunk_size = { - let chunk_size_str = chunk_size_buf.trim_matches(|c| c == '\r' || c == '\n'); - - u64::from_str_radix(chunk_size_str, 16) - .map_err(|err| io::Error::new(io::ErrorKind::InvalidInput, err))? - }; - - if chunk_size == 0 { - break; - } - - io::copy(&mut (&mut reader).take(chunk_size), &mut self.body)?; - - chunk_size_buf.clear(); - reader.read_line(&mut chunk_size_buf)?; - } - - return Ok(()); + pub async fn read_body(&mut self) -> &Vec { + if self.body.is_none() { + let raw_body = self.inner.body_mut(); + let mut buf = body::aggregate(raw_body) + .await + .map_err(|err| Error::new_with_context(ErrorKind::RequestBodyFailure, err)) + .unwrap(); + let bytes = buf.copy_to_bytes(buf.remaining()).to_vec(); + self.body = Some(bytes); } - if self.version == (1, 0) { - return io::copy(&mut raw_body_rd, &mut self.body).map(|_| ()); - } - - Ok(()) - } -} - -impl Default for Request { - fn default() -> Self { - Self { - version: (1, 1), - method: String::new(), - path: String::new(), - headers: Vec::new(), - body: Vec::new(), - error: None, - is_parsed: false, - last_header_field: None, - last_header_value: None, - } + self.body.as_ref().unwrap() } -} - -impl<'a> From<&'a TcpStream> for Request { - fn from(mut stream: &TcpStream) -> Self { - let mut request = Self::default(); - - let mut all_buf = Vec::new(); - - loop { - if request.is_parsed { - break; - } - - let mut headers = [httparse::EMPTY_HEADER; 16]; - let mut req = httparse::Request::new(&mut headers); - let mut buf = [0; 1024]; - - let rlen = match stream.read(&mut buf) { - Err(e) => Err(e.to_string()), - Ok(0) => Err("Nothing to read.".into()), - Ok(i) => Ok(i), - } - .map_err(|e| request.error = Some(e)) - .unwrap_or(0); - - if rlen == 0 { - break; - } - - all_buf.extend_from_slice(&buf[..rlen]); - let _ = req - .parse(&all_buf) - .map_err(|e| { - request.error = Some(e.to_string()); - request.is_parsed = true; - }) - .map(|status| match status { - httparse::Status::Complete(head_length) => { - if let Some(a @ 0..=1) = req.version { - request.version = (1, a); - } - - if let Some(a) = req.method { - request.method += a; - } - - if let Some(a) = req.path { - request.path += a - } - - for h in req.headers { - request.last_header_field = Some(h.name.to_lowercase()); - request.last_header_value = - Some(String::from_utf8_lossy(h.value).to_string()); - - request.record_last_header(); - } - - let raw_body_rd = Cursor::new(&all_buf[head_length..]).chain(stream); - - if let Err(err) = request.read_request_body(raw_body_rd) { - request.error = Some(err.to_string()); - } - - request.is_parsed = true; - } - httparse::Status::Partial => (), - }); + #[allow(clippy::wrong_self_convention)] + pub(crate) async fn to_string(&mut self) -> String { + let mut formatted = format!( + "\r\n{} {}\r\n", + &self.inner.method(), + &self.inner.uri().path() + ); + + for (key, value) in self.inner.headers() { + formatted.push_str(&format!( + "{}: {}\r\n", + key, + value.to_str().unwrap_or("") + )); } - request - } -} + let body = self.read_body().await; -impl fmt::Display for Request { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "\r\n{} {}\r\n", &self.method, &self.path)?; - - for &(ref key, ref value) in &self.headers { - writeln!(f, "{}: {}\r", key, value)?; + if !body.is_empty() { + formatted.push_str(&format!("{}\r\n", &String::from_utf8_lossy(body))); } - if self.body.is_empty() { - write!(f, "") - } else { - writeln!(f, "{}\r", &String::from_utf8_lossy(&self.body)) - } + formatted } } diff --git a/src/response.rs b/src/response.rs index 52c8b35..ead4113 100644 --- a/src/response.rs +++ b/src/response.rs @@ -1,11 +1,14 @@ -use std::convert::From; +use core::task::Poll; +use futures::stream::Stream; +use hyper::StatusCode; use std::fmt; use std::io; +use std::mem; use std::sync::Arc; #[derive(Clone, Debug, PartialEq)] pub(crate) struct Response { - pub status: Status, + pub status: StatusCode, pub headers: Vec<(String, String)>, pub body: Body, } @@ -43,254 +46,57 @@ impl PartialEq for Body { impl Default for Response { fn default() -> Self { Self { - status: Status::Ok, + status: StatusCode::OK, headers: vec![("connection".into(), "close".into())], body: Body::Bytes(Vec::new()), } } } -pub(crate) struct Chunked { - writer: W, +pub(crate) struct Chunked { + buffer: Vec, + finished: bool, } -impl Chunked { - pub fn new(writer: W) -> Self { - Self { writer } - } - - pub fn finish(mut self) -> io::Result { - self.writer.write_all(b"0\r\n\r\n")?; - Ok(self.writer) - } -} - -impl io::Write for Chunked { - fn write(&mut self, buf: &[u8]) -> io::Result { - if buf.is_empty() { - return Ok(0); +impl Chunked { + pub fn new() -> Self { + Self { + buffer: vec![], + finished: false, } - self.writer - .write_all(format!("{:x}\r\n", buf.len()).as_bytes())?; - self.writer.write_all(buf)?; - self.writer.write_all(b"\r\n")?; - Ok(buf.len()) } - fn flush(&mut self) -> io::Result<()> { - self.writer.flush() + pub fn finish(&mut self) { + self.finished = true; } } -#[derive(Clone, Debug, PartialEq)] -#[cfg_attr(feature = "cargo-clippy", allow(clippy::enum_variant_names))] -pub enum Status { - Continue, - SwitchingProtocols, - Processing, - Ok, - Created, - Accepted, - NonAuthoritativeInformation, - NoContent, - ResetContent, - PartialContent, - MultiStatus, - AlreadyReported, - IMUsed, - MultipleChoices, - MovedPermanently, - Found, - SeeOther, - NotModified, - UseProxy, - TemporaryRedirect, - PermanentRedirect, - BadRequest, - Unauthorized, - PaymentRequired, - Forbidden, - NotFound, - MethodNotAllowed, - NotAcceptable, - ProxyAuthenticationRequired, - RequestTimeout, - Conflict, - Gone, - LengthRequired, - PreconditionFailed, - PayloadTooLarge, - RequestURITooLong, - UnsupportedMediaType, - RequestedRangeNotSatisfiable, - ExpectationFailed, - ImATeapot, - MisdirectedRequest, - UnprocessableEntity, - Locked, - FailedDependency, - UpgradeRequired, - PreconditionRequired, - TooManyRequests, - RequestHeaderFieldsTooLarge, - ConnectionClosedWithoutResponse, - UnavailableForLegalReasons, - ClientClosedRequest, - InternalServerError, - NotImplemented, - BadGateway, - ServiceUnavailable, - GatewayTimeout, - HTTPVersionNotSupported, - VariantAlsoNegotiates, - InsufficientStorage, - LoopDetected, - NotExtended, - NetworkAuthenticationRequired, - NetworkConnectTimeoutError, - Custom(String), -} +impl Stream for Chunked { + type Item = Result, String>; -impl From for Status { - fn from(status_code: usize) -> Self { - match status_code { - 100 => Self::Continue, - 101 => Self::SwitchingProtocols, - 102 => Self::Processing, - 200 => Self::Ok, - 201 => Self::Created, - 202 => Self::Accepted, - 203 => Self::NonAuthoritativeInformation, - 204 => Self::NoContent, - 205 => Self::ResetContent, - 206 => Self::PartialContent, - 207 => Self::MultiStatus, - 208 => Self::AlreadyReported, - 226 => Self::IMUsed, - 300 => Self::MultipleChoices, - 301 => Self::MovedPermanently, - 302 => Self::Found, - 303 => Self::SeeOther, - 304 => Self::NotModified, - 305 => Self::UseProxy, - 307 => Self::TemporaryRedirect, - 308 => Self::PermanentRedirect, - 400 => Self::BadRequest, - 401 => Self::Unauthorized, - 402 => Self::PaymentRequired, - 403 => Self::Forbidden, - 404 => Self::NotFound, - 405 => Self::MethodNotAllowed, - 406 => Self::NotAcceptable, - 407 => Self::ProxyAuthenticationRequired, - 408 => Self::RequestTimeout, - 409 => Self::Conflict, - 410 => Self::Gone, - 411 => Self::LengthRequired, - 412 => Self::PreconditionFailed, - 413 => Self::PayloadTooLarge, - 414 => Self::RequestURITooLong, - 415 => Self::UnsupportedMediaType, - 416 => Self::RequestedRangeNotSatisfiable, - 417 => Self::ExpectationFailed, - 418 => Self::ImATeapot, - 421 => Self::MisdirectedRequest, - 422 => Self::UnprocessableEntity, - 423 => Self::Locked, - 424 => Self::FailedDependency, - 426 => Self::UpgradeRequired, - 428 => Self::PreconditionRequired, - 429 => Self::TooManyRequests, - 431 => Self::RequestHeaderFieldsTooLarge, - 444 => Self::ConnectionClosedWithoutResponse, - 451 => Self::UnavailableForLegalReasons, - 499 => Self::ClientClosedRequest, - 500 => Self::InternalServerError, - 501 => Self::NotImplemented, - 502 => Self::BadGateway, - 503 => Self::ServiceUnavailable, - 504 => Self::GatewayTimeout, - 505 => Self::HTTPVersionNotSupported, - 506 => Self::VariantAlsoNegotiates, - 507 => Self::InsufficientStorage, - 508 => Self::LoopDetected, - 510 => Self::NotExtended, - 511 => Self::NetworkAuthenticationRequired, - 599 => Self::NetworkConnectTimeoutError, - _ => Status::Custom(format!("{} Custom", status_code)), + fn poll_next( + mut self: std::pin::Pin<&mut Self>, + _cx: &mut std::task::Context<'_>, + ) -> std::task::Poll> { + if !self.buffer.is_empty() { + let data = mem::take(&mut self.buffer); + Poll::Ready(Some(Ok(data))) + } else if !self.finished { + Poll::Pending + } else { + Poll::Ready(None) } } } -impl fmt::Display for Status { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - let formatted = match self { - Status::Continue => "100 Continue", - Status::SwitchingProtocols => "101 Switching Protocols", - Status::Processing => "102 Processing", - Status::Ok => "200 OK", - Status::Created => "201 Created", - Status::Accepted => "202 Accepted", - Status::NonAuthoritativeInformation => "203 Non-Authoritative Information", - Status::NoContent => "204 No Content", - Status::ResetContent => "205 Reset Content", - Status::PartialContent => "206 Partial Content", - Status::MultiStatus => "207 Multi-Status", - Status::AlreadyReported => "208 Already Reported", - Status::IMUsed => "226 IM Used", - Status::MultipleChoices => "300 Multiple Choices", - Status::MovedPermanently => "301 Moved Permanently", - Status::Found => "302 Found", - Status::SeeOther => "303 See Other", - Status::NotModified => "304 Not Modified", - Status::UseProxy => "305 Use Proxy", - Status::TemporaryRedirect => "307 Temporary Redirect", - Status::PermanentRedirect => "308 Permanent Redirect", - Status::BadRequest => "400 Bad Request", - Status::Unauthorized => "401 Unauthorized", - Status::PaymentRequired => "402 Payment Required", - Status::Forbidden => "403 Forbidden", - Status::NotFound => "404 Not Found", - Status::MethodNotAllowed => "405 Method Not Allowed", - Status::NotAcceptable => "406 Not Acceptable", - Status::ProxyAuthenticationRequired => "407 Proxy Authentication Required", - Status::RequestTimeout => "408 Request Timeout", - Status::Conflict => "409 Conflict", - Status::Gone => "410 Gone", - Status::LengthRequired => "411 Length Required", - Status::PreconditionFailed => "412 Precondition Failed", - Status::PayloadTooLarge => "413 Payload Too Large", - Status::RequestURITooLong => "414 Request-URI Too Long", - Status::UnsupportedMediaType => "415 Unsupported Media Type", - Status::RequestedRangeNotSatisfiable => "416 Requested Range Not Satisfiable", - Status::ExpectationFailed => "417 Expectation Failed", - Status::ImATeapot => "418 I'm a teapot", - Status::MisdirectedRequest => "421 Misdirected Request", - Status::UnprocessableEntity => "422 Unprocessable Entity", - Status::Locked => "423 Locked", - Status::FailedDependency => "424 Failed Dependency", - Status::UpgradeRequired => "426 Upgrade Required", - Status::PreconditionRequired => "428 Precondition Required", - Status::TooManyRequests => "429 Too Many Requests", - Status::RequestHeaderFieldsTooLarge => "431 Request Header Fields Too Large", - Status::ConnectionClosedWithoutResponse => "444 Connection Closed Without Response", - Status::UnavailableForLegalReasons => "451 Unavailable For Legal Reasons", - Status::ClientClosedRequest => "499 Client Closed Request", - Status::InternalServerError => "500 Internal Server Error", - Status::NotImplemented => "501 Not Implemented", - Status::BadGateway => "502 Bad Gateway", - Status::ServiceUnavailable => "503 Service Unavailable", - Status::GatewayTimeout => "504 Gateway Timeout", - Status::HTTPVersionNotSupported => "505 HTTP Version Not Supported", - Status::VariantAlsoNegotiates => "506 Variant Also Negotiates", - Status::InsufficientStorage => "507 Insufficient Storage", - Status::LoopDetected => "508 Loop Detected", - Status::NotExtended => "510 Not Extended", - Status::NetworkAuthenticationRequired => "511 Network Authentication Required", - Status::NetworkConnectTimeoutError => "599 Network Connect Timeout Error", - Status::Custom(ref status_code) => status_code, - }; +impl io::Write for Chunked { + fn write(&mut self, buf: &[u8]) -> io::Result { + self.buffer.append(&mut buf.to_vec()); + Ok(buf.len()) + } - write!(f, "{}", formatted) + fn flush(&mut self) -> io::Result<()> { + self.finished = true; + Ok(()) } } diff --git a/src/server.rs b/src/server.rs index 5b6ae23..922d462 100644 --- a/src/server.rs +++ b/src/server.rs @@ -1,266 +1,363 @@ -use crate::response::{Body, Chunked}; -use crate::{Mock, Request, SERVER_ADDRESS_INTERNAL}; -use std::fmt::Display; -use std::io; -use std::io::Write; -use std::net::{SocketAddr, TcpListener, TcpStream}; -use std::sync::mpsc; -use std::sync::Mutex; +use crate::command::Command; +use crate::mock::InnerMock; +use crate::request::Request; +use crate::response::{Body as ResponseBody, Chunked as ResponseChunked}; +use crate::server_pool::SERVER_POOL; +use crate::{Error, ErrorKind, Matcher, Mock}; +use futures::stream::{self, StreamExt}; +use hyper::service::{make_service_fn, service_fn}; +use hyper::{Body, Request as HyperRequest, Response, Server as HyperServer, StatusCode}; +use std::net::{SocketAddr, TcpListener}; +use std::ops::DerefMut; +use std::sync::Arc; use std::thread; +use tokio::sync::mpsc::{self, Receiver, Sender}; +use tokio::sync::Mutex; +use tokio::task::LocalSet; + +#[derive(Clone, Debug)] +pub(crate) struct RemoteMock { + pub(crate) inner: InnerMock, +} + +impl RemoteMock { + pub(crate) fn new(inner: InnerMock) -> Self { + RemoteMock { inner } + } + + async fn matches(&self, other: &mut Request) -> bool { + self.method_matches(other) + && self.path_matches(other) + && self.headers_match(other) + && self.body_matches(other).await + } -impl Mock { fn method_matches(&self, request: &Request) -> bool { - self.method == request.method + self.inner.method.as_str() == request.method() } fn path_matches(&self, request: &Request) -> bool { - self.path.matches_value(&request.path) + self.inner.path.matches_value(request.path_and_query()) } fn headers_match(&self, request: &Request) -> bool { - self.headers.iter().all(|&(ref field, ref expected)| { - expected.matches_values(&request.find_header_values(field)) - }) + self.inner + .headers + .iter() + .all(|&(ref field, ref expected)| expected.matches_values(&request.header(field))) } - fn body_matches(&self, request: &Request) -> bool { - let raw_body = &request.body; - let safe_body = &String::from_utf8_lossy(raw_body); + async fn body_matches(&self, request: &mut Request) -> bool { + let body = request.read_body().await; + let safe_body = &String::from_utf8_lossy(body); - self.body.matches_value(safe_body) || self.body.matches_binary_value(raw_body) + self.inner.body.matches_value(safe_body) || self.inner.body.matches_binary_value(body) } #[allow(clippy::missing_const_for_fn)] fn is_missing_hits(&self) -> bool { - match (self.expected_hits_at_least, self.expected_hits_at_most) { - (Some(_at_least), Some(at_most)) => self.hits < at_most, - (Some(at_least), None) => self.hits < at_least, - (None, Some(at_most)) => self.hits < at_most, - (None, None) => self.hits < 1, + match ( + self.inner.expected_hits_at_least, + self.inner.expected_hits_at_most, + ) { + (Some(_at_least), Some(at_most)) => self.inner.hits < at_most, + (Some(at_least), None) => self.inner.hits < at_least, + (None, Some(at_most)) => self.inner.hits < at_most, + (None, None) => self.inner.hits < 1, } } } -impl<'a> PartialEq for &'a mut Mock { - fn eq(&self, other: &Request) -> bool { - self.method_matches(other) - && self.path_matches(other) - && self.headers_match(other) - && self.body_matches(other) - } -} - -pub struct State { - pub listening_addr: Option, - pub mocks: Vec, - pub unmatched_requests: Vec, +#[derive(Debug)] +pub(crate) struct State { + pub(crate) mocks: Vec, + pub(crate) unmatched_requests: Vec, } impl State { - #[allow(clippy::missing_const_for_fn)] fn new() -> Self { - Self { - listening_addr: None, - mocks: Vec::new(), - unmatched_requests: Vec::new(), + State { + mocks: vec![], + unmatched_requests: vec![], } } } -lazy_static! { - pub static ref STATE: Mutex = Mutex::new(State::new()); -} - -/// Address and port of the local server. -/// Can be used with `std::net::TcpStream`. /// -/// The server will be started if necessary. -pub fn address() -> SocketAddr { - try_start(); - - let state = STATE.lock().map(|state| state.listening_addr); - state - .expect("state lock") - .expect("server should be listening") -} - -/// A local `http://…` URL of the server. +/// One instance of the mock server. /// -/// The server will be started if necessary. -pub fn url() -> String { - format!("http://{}", address()) +/// Mockito uses a server pool to manage running servers. Once the pool reaches capacity, +/// new requests will have to wait for a free server. The size of the server pool +/// is set to 100. +/// +/// Most of the times, you should initialize new servers with `Server::new`, which fetches +/// the next available instance from the pool: +/// +/// ``` +/// 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`: +/// +/// ``` +/// let mut server = mockito::Server::new_with_port(0); +/// ``` +/// +#[derive(Debug)] +pub struct Server { + address: String, + state: Arc>, + sender: Sender, } -pub fn try_start() { - let mut state = STATE.lock().unwrap(); +impl Server { + /// + /// Fetches a new mock server from the server pool. + /// + /// This method will panic on failure. + /// + /// If for any reason you'd like to bypass the server pool, you can use `Server::new_with_port`: + /// + #[track_caller] + pub fn new() -> impl DerefMut { + Server::try_new().unwrap() + } - if state.listening_addr.is_some() { - return; + /// + /// Same as `Server::new` but async. + /// + pub async fn new_async() -> impl DerefMut { + SERVER_POOL.get().await.unwrap() } - let (tx, rx) = mpsc::channel(); + /// + /// Same as `Server::new` but won't panic on failure. + /// + pub(crate) fn try_new() -> Result, Error> { + crate::RUNTIME.block_on(async { Server::try_new_async().await }) + } - thread::spawn(move || { - let res = TcpListener::bind(SERVER_ADDRESS_INTERNAL).or_else(|err| { - warn!("{}", err); - TcpListener::bind("127.0.0.1:0") - }); - let (listener, addr) = match res { - Ok(listener) => { - let addr = listener.local_addr().unwrap(); - tx.send(Some(addr)).unwrap(); - (listener, addr) - } - Err(err) => { - error!("{}", err); - tx.send(None).unwrap(); - return; + /// + /// Same as `Server::try_new` but async. + /// + pub(crate) async fn try_new_async() -> Result, Error> { + SERVER_POOL + .get() + .await + .map_err(|err| Error::new_with_context(ErrorKind::ServerFailure, err)) + } + + /// + /// 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**. + /// + /// This method will panic on failure. + /// + #[track_caller] + pub fn new_with_port(port: u16) -> Server { + Server::try_new_with_port(port).unwrap() + } + + /// + /// Same as `Server::try_new_with_port_async` but async. + /// + pub async fn new_with_port_async(port: u16) -> Server { + Server::try_new_with_port_async(port).await.unwrap() + } + + /// + /// Same as `Server::new_with_port` but won't panic on failure. + /// + pub(crate) fn try_new_with_port(port: u16) -> Result { + crate::RUNTIME.block_on(async { Server::try_new_with_port_async(port).await }) + } + + /// + /// Same as `Server::try_new_with_port` but async. + /// + pub(crate) async fn try_new_with_port_async(port: u16) -> Result { + let state = Arc::new(Mutex::new(State::new())); + let address = SocketAddr::from(([127, 0, 0, 1], port)); + + let listener = TcpListener::bind(address) + .map_err(|err| Error::new_with_context(ErrorKind::ServerFailure, err))?; + + let address = listener + .local_addr() + .map_err(|err| Error::new_with_context(ErrorKind::ServerFailure, err))?; + + let mutex = state.clone(); + let service = make_service_fn(move |_conn| { + let mutex = mutex.clone(); + async move { + Ok::<_, Error>(service_fn(move |request: HyperRequest| { + handle_request(request, mutex.clone()) + })) } + }); + + let server = HyperServer::from_tcp(listener) + .map_err(|err| Error::new_with_context(ErrorKind::ServerFailure, err))? + .serve(service); + + thread::spawn(move || LocalSet::new().block_on(&crate::RUNTIME, server)); + + let (sender, receiver) = mpsc::channel(32); + + let mut server = Server { + address: address.to_string(), + state, + sender, }; - debug!("Server is listening at {}", addr); - for stream in listener.incoming() { - if let Ok(stream) = stream { - let request = Request::from(&stream); - debug!("Request received: {}", request); - if request.is_ok() { - handle_request(request, stream); - } else { - let message = request - .error() - .map_or("Could not parse the request.", |err| err.as_str()); - debug!("Could not parse request because: {}", message); - respond_with_error(stream, request.version, message); - } - } else { - debug!("Could not read from stream"); + server.accept_commands(receiver).await; + + Ok(server) + } + + /// + /// Initializes a mock with the given HTTP `method` and `path`. + /// + /// The mock is enabled on the server only after calling the `Mock::create` method. + /// + /// ## Example + /// + /// ``` + /// let mut s = mockito::Server::new(); + /// + /// let _m1 = s.mock("GET", "/"); + /// let _m2 = s.mock("POST", "/users"); + /// let _m3 = s.mock("DELETE", "/users?id=1"); + /// ``` + /// + pub fn mock>(&mut self, method: &str, path: P) -> Mock { + Mock::new(self.sender.clone(), method, path) + } + + /// + /// The URL of the mock server (including the protocol). + /// + pub fn url(&self) -> String { + format!("http://{}", self.address) + } + + /// + /// The host and port of the mock server. + /// Can be used with `std::net::TcpStream`. + /// + pub fn host_with_port(&self) -> String { + self.address.clone() + } + + /// + /// Removes all the mocks stored on the server. + /// + pub fn reset(&mut self) { + futures::executor::block_on(async { self.reset_async().await }) + } + + /// + /// Same as `Server::reset` but async. + /// + pub async fn reset_async(&mut self) { + let state = self.state.clone(); + let mut state = state.lock().await; + state.mocks.clear(); + state.unmatched_requests.clear(); + } + + async fn accept_commands(&mut self, mut receiver: Receiver) { + let state = self.state.clone(); + tokio::spawn(async move { + while let Some(cmd) = receiver.recv().await { + let state = state.lock().await; + Command::handle(cmd, state).await; } - } - }); + }); - state.listening_addr = rx.recv().ok().and_then(|addr| addr); + log::debug!("Server is accepting commands"); + } } -fn handle_request(request: Request, stream: TcpStream) { - handle_match_mock(request, stream); -} +async fn handle_request( + hyper_request: HyperRequest, + state: Arc>, +) -> Result, Error> { + let mut request = Request::new(hyper_request); + log::debug!("Request received: {}", request.to_string().await); -fn handle_match_mock(request: Request, stream: TcpStream) { - let mut state = STATE.lock().unwrap(); + let mutex = state.clone(); + let mut state = mutex.lock().await; - let mut matchings_mocks = state - .mocks - .iter_mut() - .filter(|mock| mock == &request) - .collect::>(); + let mut mocks_stream = stream::iter(&mut state.mocks); + let mut matching_mocks: Vec<&mut RemoteMock> = vec![]; - let maybe_missing_hits = matchings_mocks.iter_mut().find(|m| m.is_missing_hits()); + while let Some(mock) = mocks_stream.next().await { + if mock.matches(&mut request).await { + matching_mocks.push(mock); + } + } + + let maybe_missing_hits = matching_mocks.iter_mut().find(|m| m.is_missing_hits()); let mock = match maybe_missing_hits { Some(m) => Some(m), - None => matchings_mocks.last_mut(), + None => matching_mocks.last_mut(), }; if let Some(mock) = mock { - debug!("Mock found"); - mock.hits += 1; - respond_with_mock(stream, request.version, mock, request.is_head()); + log::debug!("Mock found"); + mock.inner.hits += 1; + respond_with_mock(request, mock).await } else { - debug!("Mock not found"); - respond_with_mock_not_found(stream, request.version); + log::debug!("Mock not found"); state.unmatched_requests.push(request); + respond_with_mock_not_found() } } -fn respond( - stream: TcpStream, - version: (u8, u8), - status: impl Display, - headers: Option<&Vec<(String, String)>>, - body: Option<&str>, -) { - let body = body.map(|s| Body::Bytes(s.as_bytes().to_owned())); - if let Err(e) = respond_bytes(stream, version, status, headers, body.as_ref()) { - eprintln!("warning: Mock response write error: {}", e); - } -} - -fn respond_bytes( - mut stream: TcpStream, - version: (u8, u8), - status: impl Display, - headers: Option<&Vec<(String, String)>>, - body: Option<&Body>, -) -> io::Result<()> { - let mut response = Vec::from(format!("HTTP/{}.{} {}\r\n", version.0, version.1, status)); - let mut has_content_length_header = false; - - if let Some(headers) = headers { - for &(ref key, ref value) in headers { - response.extend(key.as_bytes()); - response.extend(b": "); - response.extend(value.as_bytes()); - response.extend(b"\r\n"); - } +async fn respond_with_mock(request: Request, mock: &RemoteMock) -> Result, Error> { + let status: StatusCode = mock.inner.response.status; + let mut response = Response::builder().status(status); - has_content_length_header = headers.iter().any(|(key, _)| key == "content-length"); + for (name, value) in mock.inner.response.headers.iter() { + response = response.header(name, value); } - match body { - Some(Body::Bytes(bytes)) => { - if !has_content_length_header { - response.extend(format!("content-length: {}\r\n", bytes.len()).as_bytes()); + let body = if request.method() != "HEAD" { + match &mock.inner.response.body { + ResponseBody::Bytes(bytes) => { + if !request.has_header("content-length") { + response = response.header("content-length", bytes.len()); + } + Body::from(bytes.clone()) + } + ResponseBody::Fn(body_fn) => { + let mut chunked = ResponseChunked::new(); + body_fn(&mut chunked) + .map_err(|_| Error::new(ErrorKind::ResponseBodyFailure)) + .unwrap(); + chunked.finish(); + + Body::wrap_stream(chunked) } } - Some(Body::Fn(_)) => { - response.extend(b"transfer-encoding: chunked\r\n"); - } - None => {} - }; - response.extend(b"\r\n"); - stream.write_all(&response)?; - match body { - Some(Body::Bytes(bytes)) => { - stream.write_all(bytes)?; - } - Some(Body::Fn(cb)) => { - let mut chunked = Chunked::new(&mut stream); - cb(&mut chunked)?; - chunked.finish()?; - } - None => {} - }; - stream.flush() -} - -fn respond_with_mock(stream: TcpStream, version: (u8, u8), mock: &Mock, skip_body: bool) { - let body = if skip_body { - None } else { - Some(&mock.response.body) + Body::empty() }; - if let Err(e) = respond_bytes( - stream, - version, - &mock.response.status, - Some(&mock.response.headers), - body, - ) { - eprintln!("warning: Mock response write error: {}", e); - } -} + let response: Response = response + .body(body) + .map_err(|err| Error::new_with_context(ErrorKind::ResponseFailure, err))?; -fn respond_with_mock_not_found(stream: TcpStream, version: (u8, u8)) { - respond( - stream, - version, - "501 Mock Not Found", - Some(&vec![("content-length".into(), "0".into())]), - None, - ); + Ok(response) } -fn respond_with_error(stream: TcpStream, version: (u8, u8), message: &str) { - respond(stream, version, "422 Mock Error", None, Some(message)); +fn respond_with_mock_not_found() -> Result, Error> { + let response: Response = Response::builder() + .status(StatusCode::NOT_IMPLEMENTED) + .body(Body::empty()) + .map_err(|err| Error::new_with_context(ErrorKind::ResponseFailure, err))?; + + Ok(response) } diff --git a/src/server_pool.rs b/src/server_pool.rs new file mode 100644 index 0000000..1effe2f --- /dev/null +++ b/src/server_pool.rs @@ -0,0 +1,38 @@ +use crate::Error; +use crate::Server; +use async_trait::async_trait; +use deadpool::managed::{self, Pool}; +use lazy_static::lazy_static; + +const DEFAULT_POOL_SIZE: usize = 100; + +lazy_static! { + pub(crate) static ref SERVER_POOL: Pool = ServerPool::new(DEFAULT_POOL_SIZE); +} + +pub(crate) struct ServerPool {} + +impl ServerPool { + fn new(max_size: usize) -> Pool { + let server_pool = ServerPool {}; + Pool::builder(server_pool) + .max_size(max_size) + .build() + .expect("Could not create server pool") + } +} + +#[async_trait] +impl managed::Manager for ServerPool { + type Type = Server; + type Error = Error; + + async fn create(&self) -> Result { + Server::try_new_with_port_async(0).await + } + + async fn recycle(&self, server: &mut Server) -> managed::RecycleResult { + server.reset_async().await; + Ok(()) + } +} diff --git a/tests/legacy.rs b/tests/legacy.rs new file mode 100644 index 0000000..40e0cdd --- /dev/null +++ b/tests/legacy.rs @@ -0,0 +1,1616 @@ +#[macro_use] +extern crate serde_json; + +#[allow(deprecated)] +use mockito::{mock, server_address, Matcher}; +use rand::distributions::Alphanumeric; +use rand::Rng; +use std::fs; +use std::io::{BufRead, BufReader, Read, Write}; +use std::mem; +use std::net::TcpStream; +use std::path::Path; +use std::str::FromStr; +use std::thread; + +type Binary = Vec; + +#[allow(deprecated)] +fn request_stream>( + version: &str, + route: &str, + headers: &str, + body: StrOrBytes, +) -> TcpStream { + let mut stream = TcpStream::connect(server_address()).unwrap(); + let mut message: Binary = Vec::new(); + for b in [route, " HTTP/", version, "\r\n", headers, "\r\n"] + .join("") + .as_bytes() + { + message.push(*b); + } + for b in body.as_ref().iter() { + message.push(*b); + } + + stream.write_all(&message).unwrap(); + + stream +} + +fn parse_stream(stream: TcpStream, skip_body: bool) -> (String, Vec, Binary) { + let mut reader = BufReader::new(stream); + + let mut status_line = String::new(); + reader.read_line(&mut status_line).unwrap(); + + let mut headers = vec![]; + let mut content_length: Option = None; + let mut is_chunked = false; + loop { + let mut header_line = String::new(); + reader.read_line(&mut header_line).unwrap(); + + if header_line == "\r\n" { + break; + } + + if header_line.starts_with("transfer-encoding:") && header_line.contains("chunked") { + is_chunked = true; + } + + if header_line.starts_with("content-length:") { + let mut parts = header_line.split(':'); + content_length = Some(u64::from_str(parts.nth(1).unwrap().trim()).unwrap()); + } + + headers.push(header_line.trim_end().to_string()); + } + + let mut body: Binary = Vec::new(); + if !skip_body { + if let Some(content_length) = content_length { + reader.take(content_length).read_to_end(&mut body).unwrap(); + } else if is_chunked { + let mut chunk_size_buf = String::new(); + loop { + chunk_size_buf.clear(); + reader.read_line(&mut chunk_size_buf).unwrap(); + + let chunk_size = u64::from_str_radix( + chunk_size_buf.trim_matches(|c| c == '\r' || c == '\n'), + 16, + ) + .expect("chunk size"); + if chunk_size == 0 { + break; + } + + (&mut reader) + .take(chunk_size) + .read_to_end(&mut body) + .unwrap(); + + chunk_size_buf.clear(); + reader.read_line(&mut chunk_size_buf).unwrap(); + } + } + } + + (status_line, headers, body) +} + +fn binary_request>( + route: &str, + headers: &str, + body: StrOrBytes, +) -> (String, Vec, Binary) { + parse_stream( + request_stream("1.1", route, headers, body), + route.starts_with("HEAD"), + ) +} + +fn request(route: &str, headers: &str) -> (String, Vec, String) { + let (status, headers, body) = binary_request(route, headers, ""); + let parsed_body: String = std::str::from_utf8(body.as_slice()).unwrap().to_string(); + (status, headers, parsed_body) +} + +fn request_with_body(route: &str, headers: &str, body: &str) -> (String, Vec, String) { + let headers = format!("{}content-length: {}\r\n", headers, body.len()); + let (status, headers, body) = binary_request(route, &headers, body); + let parsed_body: String = std::str::from_utf8(body.as_slice()).unwrap().to_string(); + (status, headers, parsed_body) +} + +#[allow(deprecated)] +#[test] +#[allow(deprecated)] +fn test_legacy_create_starts_the_server() { + let _m = mock("GET", "/").with_body("hello").create(); + + let stream = TcpStream::connect(server_address()); + assert!(stream.is_ok()); +} + +#[test] +#[allow(deprecated)] +fn test_legacy_simple_route_mock() { + let _m = mock("GET", "/hello").with_body("world").create(); + + let (status_line, _, body) = request("GET /hello", ""); + assert_eq!("HTTP/1.1 200 OK\r\n", status_line); + assert_eq!("world", body); +} + +#[test] +#[allow(deprecated)] +fn test_legacy_two_route_mocks() { + let _m1 = mock("GET", "/a").with_body("aaa").create(); + let _m2 = mock("GET", "/b").with_body("bbb").create(); + + let (_, _, body_a) = request("GET /a", ""); + + assert_eq!("aaa", body_a); + let (_, _, body_b) = request("GET /b", ""); + assert_eq!("bbb", body_b); +} + +#[test] +#[allow(deprecated)] +fn test_legacy_no_match_returns_501() { + let _m = mock("GET", "/").with_body("matched").create(); + + let (status_line, headers, _) = request("GET /nope", ""); + assert_eq!("HTTP/1.1 501 Not Implemented\r\n", status_line); + assert_eq!("content-length: 0", headers[0]); +} + +#[test] +#[allow(deprecated)] +fn test_legacy_match_header() { + let _m1 = mock("GET", "/") + .match_header("content-type", "application/json") + .with_body("{}") + .create(); + + let _m2 = mock("GET", "/") + .match_header("content-type", "text/plain") + .with_body("hello") + .create(); + + let (_, _, body_json) = request("GET /", "content-type: application/json\r\n"); + assert_eq!("{}", body_json); + + let (_, _, body_text) = request("GET /", "content-type: text/plain\r\n"); + assert_eq!("hello", body_text); +} + +#[test] +#[allow(deprecated)] +fn test_legacy_match_header_is_case_insensitive_on_the_field_name() { + let _m = mock("GET", "/") + .match_header("content-type", "text/plain") + .create(); + + let (uppercase_status_line, _, _) = request("GET /", "Content-Type: text/plain\r\n"); + assert_eq!("HTTP/1.1 200 OK\r\n", uppercase_status_line); + + let (lowercase_status_line, _, _) = request("GET /", "content-type: text/plain\r\n"); + assert_eq!("HTTP/1.1 200 OK\r\n", lowercase_status_line); +} + +#[test] +#[allow(deprecated)] +fn test_legacy_match_multiple_headers() { + let _m = mock("GET", "/") + .match_header("Content-Type", "text/plain") + .match_header("Authorization", "secret") + .with_body("matched") + .create(); + + let (_, _, body_matching) = request( + "GET /", + "content-type: text/plain\r\nauthorization: secret\r\n", + ); + assert_eq!("matched", body_matching); + + let (status_not_matching, _, _) = request( + "GET /", + "content-type: text/plain\r\nauthorization: meh\r\n", + ); + assert_eq!("HTTP/1.1 501 Not Implemented\r\n", status_not_matching); +} + +#[test] +#[allow(deprecated)] +fn test_legacy_match_header_any_matching() { + let _m = mock("GET", "/") + .match_header("Content-Type", Matcher::Any) + .with_body("matched") + .create(); + + let (_, _, body) = request("GET /", "content-type: something\r\n"); + assert_eq!("matched", body); +} + +#[test] +#[allow(deprecated)] +fn test_legacy_match_header_any_not_matching() { + let _m = mock("GET", "/") + .match_header("Content-Type", Matcher::Any) + .with_body("matched") + .create(); + + let (status, _, _) = request("GET /", ""); + assert_eq!("HTTP/1.1 501 Not Implemented\r\n", status); +} + +#[test] +#[allow(deprecated)] +fn test_legacy_match_header_missing_matching() { + let _m = mock("GET", "/") + .match_header("Authorization", Matcher::Missing) + .create(); + + let (status, _, _) = request("GET /", ""); + assert_eq!("HTTP/1.1 200 OK\r\n", status); +} + +#[test] +#[allow(deprecated)] +fn test_legacy_match_header_missing_not_matching() { + let _m = mock("GET", "/") + .match_header("Authorization", Matcher::Missing) + .create(); + + let (status, _, _) = request("GET /", "Authorization: something\r\n"); + assert_eq!("HTTP/1.1 501 Not Implemented\r\n", status); +} + +#[test] +#[allow(deprecated)] +fn test_legacy_match_header_missing_not_matching_even_when_empty() { + let _m = mock("GET", "/") + .match_header("Authorization", Matcher::Missing) + .create(); + + let (status, _, _) = request("GET /", "Authorization:\r\n"); + assert_eq!("HTTP/1.1 501 Not Implemented\r\n", status); +} + +#[test] +#[allow(deprecated)] +fn test_legacy_match_multiple_header_conditions_matching() { + let _m = mock("GET", "/") + .match_header("Hello", "World") + .match_header("Content-Type", Matcher::Any) + .match_header("Authorization", Matcher::Missing) + .create(); + + let (status, _, _) = request("GET /", "Hello: World\r\nContent-Type: something\r\n"); + assert_eq!("HTTP/1.1 200 OK\r\n", status); +} + +#[test] +#[allow(deprecated)] +fn test_legacy_match_multiple_header_conditions_not_matching() { + let _m = mock("GET", "/") + .match_header("hello", "world") + .match_header("Content-Type", Matcher::Any) + .match_header("Authorization", Matcher::Missing) + .create(); + + let (status, _, _) = request("GET /", "Hello: World\r\n"); + assert_eq!("HTTP/1.1 501 Not Implemented\r\n", status); +} + +#[test] +#[allow(deprecated)] +fn test_legacy_match_any_body_by_default() { + let _m = mock("POST", "/").create(); + + let (status, _, _) = request_with_body("POST /", "", "hello"); + assert_eq!("HTTP/1.1 200 OK\r\n", status); +} + +#[test] +#[allow(deprecated)] +fn test_legacy_match_body() { + let _m = mock("POST", "/").match_body("hello").create(); + + let (status, _, _) = request_with_body("POST /", "", "hello"); + assert_eq!("HTTP/1.1 200 OK\r\n", status); +} + +#[test] +#[allow(deprecated)] +fn test_legacy_match_body_not_matching() { + let _m = mock("POST", "/").match_body("hello").create(); + + let (status, _, _) = request_with_body("POST /", "", "bye"); + assert_eq!("HTTP/1.1 501 Not Implemented\r\n", status); +} + +#[test] +#[allow(deprecated)] +fn test_legacy_match_binary_body() { + let _m = mock("POST", "/") + .match_body(Path::new("./tests/files/test_payload.bin")) + .create(); + + let mut file_content: Binary = Vec::new(); + fs::File::open("./tests/files/test_payload.bin") + .unwrap() + .read_to_end(&mut file_content) + .unwrap(); + let content_length_header = format!("Content-Length: {}\r\n", file_content.len()); + let (status, _, _) = binary_request("POST /", &content_length_header, file_content); + assert_eq!("HTTP/1.1 200 OK\r\n", status); +} + +#[test] +#[allow(deprecated)] +fn test_legacy_does_not_match_binary_body() { + let _m = mock("POST", "/") + .match_body(Path::new("./tests/files/test_payload.bin")) + .create(); + + let file_content: Binary = (0..1024).map(|_| rand::random::()).collect(); + let content_length_header = format!("Content-Length: {}\r\n", file_content.len()); + let (status, _, _) = binary_request("POST /", &content_length_header, file_content); + assert_eq!("HTTP/1.1 501 Not Implemented\r\n", status); +} + +#[test] +#[allow(deprecated)] +fn test_legacy_match_body_with_regex() { + let _m = mock("POST", "/") + .match_body(Matcher::Regex("hello".to_string())) + .create(); + + let (status, _, _) = request_with_body("POST /", "", "test hello test"); + assert_eq!("HTTP/1.1 200 OK\r\n", status); +} + +#[test] +#[allow(deprecated)] +fn test_legacy_match_body_with_regex_not_matching() { + let _m = mock("POST", "/") + .match_body(Matcher::Regex("hello".to_string())) + .create(); + + let (status, _, _) = request_with_body("POST /", "", "bye"); + assert_eq!("HTTP/1.1 501 Not Implemented\r\n", status); +} + +#[test] +#[allow(deprecated)] +fn test_legacy_match_body_with_json() { + let _m = mock("POST", "/") + .match_body(Matcher::Json(json!({"hello":"world", "foo": "bar"}))) + .create(); + + let (status, _, _) = request_with_body("POST /", "", r#"{"hello":"world", "foo": "bar"}"#); + assert_eq!("HTTP/1.1 200 OK\r\n", status); +} + +#[test] +#[allow(deprecated)] +fn test_legacy_match_body_with_more_headers_with_json() { + let _m = mock("POST", "/") + .match_body(Matcher::Json(json!({"hello":"world", "foo": "bar"}))) + .create(); + + let headers = (0..15) + .map(|n| { + format!( + "x-header-{}: foo-bar-value-zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz\r\n", + n + ) + }) + .collect::>() + .concat(); + + let (status, _, _) = + request_with_body("POST /", &headers, r#"{"hello":"world", "foo": "bar"}"#); + assert_eq!("HTTP/1.1 200 OK\r\n", status); +} + +#[test] +#[allow(deprecated)] +fn test_legacy_match_body_with_json_order() { + let _m = mock("POST", "/") + .match_body(Matcher::Json(json!({"foo": "bar", "hello": "world"}))) + .create(); + + let (status, _, _) = request_with_body("POST /", "", r#"{"hello":"world", "foo": "bar"}"#); + assert_eq!("HTTP/1.1 200 OK\r\n", status); +} + +#[test] +#[allow(deprecated)] +fn test_legacy_match_body_with_json_string() { + let _m = mock("POST", "/") + .match_body(Matcher::JsonString( + "{\"hello\":\"world\", \"foo\": \"bar\"}".to_string(), + )) + .create(); + + let (status, _, _) = request_with_body("POST /", "", r#"{"hello":"world", "foo": "bar"}"#); + assert_eq!("HTTP/1.1 200 OK\r\n", status); +} + +#[test] +#[allow(deprecated)] +fn test_legacy_match_body_with_json_string_order() { + let _m = mock("POST", "/") + .match_body(Matcher::JsonString( + "{\"foo\": \"bar\", \"hello\": \"world\"}".to_string(), + )) + .create(); + + let (status, _, _) = request_with_body("POST /", "", r#"{"hello":"world", "foo": "bar"}"#); + assert_eq!("HTTP/1.1 200 OK\r\n", status); +} + +#[test] +#[allow(deprecated)] +fn test_legacy_match_body_with_partial_json() { + let _m = mock("POST", "/") + .match_body(Matcher::PartialJson(json!({"hello":"world"}))) + .create(); + + let (status, _, _) = request_with_body("POST /", "", r#"{"hello":"world", "foo": "bar"}"#); + assert_eq!("HTTP/1.1 200 OK\r\n", status); +} + +#[test] +#[allow(deprecated)] +fn test_legacy_match_body_with_partial_json_and_extra_fields() { + let _m = mock("POST", "/") + .match_body(Matcher::PartialJson(json!({"hello":"world", "foo": "bar"}))) + .create(); + + let (status, _, _) = request_with_body("POST /", "", r#"{"hello":"world"}"#); + assert_eq!("HTTP/1.1 501 Not Implemented\r\n", status); +} + +#[test] +#[allow(deprecated)] +fn test_legacy_match_body_with_partial_json_string() { + let _m = mock("POST", "/") + .match_body(Matcher::PartialJsonString( + "{\"hello\": \"world\"}".to_string(), + )) + .create(); + + let (status, _, _) = request_with_body("POST /", "", r#"{"hello":"world", "foo": "bar"}"#); + assert_eq!("HTTP/1.1 200 OK\r\n", status); +} + +#[test] +#[allow(deprecated)] +fn test_legacy_match_body_with_partial_json_string_and_extra_fields() { + let _m = mock("POST", "/") + .match_body(Matcher::PartialJsonString( + "{\"foo\": \"bar\", \"hello\": \"world\"}".to_string(), + )) + .create(); + + let (status, _, _) = request_with_body("POST /", "", r#"{"hello":"world"}"#); + assert_eq!("HTTP/1.1 501 Not Implemented\r\n", status); +} + +#[test] +#[allow(deprecated)] +fn test_legacy_mock_with_status() { + let _m = mock("GET", "/").with_status(204).with_body("").create(); + + let (status_line, _, _) = request("GET /", ""); + assert_eq!("HTTP/1.1 204 No Content\r\n", status_line); +} + +#[test] +#[allow(deprecated)] +fn test_legacy_mock_with_custom_status() { + let _m = mock("GET", "/").with_status(499).with_body("").create(); + + let (status_line, _, _) = request("GET /", ""); + assert_eq!("HTTP/1.1 499 \r\n", status_line); +} + +#[test] +#[allow(deprecated)] +fn test_legacy_mock_with_body() { + let _m = mock("GET", "/").with_body("hello").create(); + + let (_, _, body) = request("GET /", ""); + assert_eq!("hello", body); +} + +#[test] +#[allow(deprecated)] +fn test_legacy_mock_with_fn_body() { + let _m = mock("GET", "/") + .with_body_from_fn(|w| { + w.write_all(b"hel")?; + w.write_all(b"lo") + }) + .create(); + + let (_, _, body) = request("GET /", ""); + assert_eq!("hello", body); +} + +#[test] +#[allow(deprecated)] +fn test_legacy_mock_with_header() { + let _m = mock("GET", "/") + .with_header("content-type", "application/json") + .with_body("{}") + .create(); + + let (_, headers, _) = request("GET /", ""); + assert!(headers.contains(&"content-type: application/json".to_string())); +} + +#[test] +#[allow(deprecated)] +fn test_legacy_mock_with_multiple_headers() { + let _m = mock("GET", "/") + .with_header("content-type", "application/json") + .with_header("x-api-key", "1234") + .with_body("{}") + .create(); + + let (_, headers, _) = request("GET /", ""); + assert!(headers.contains(&"content-type: application/json".to_string())); + assert!(headers.contains(&"x-api-key: 1234".to_string())); +} + +#[test] +#[allow(deprecated)] +fn test_legacy_mock_preserves_header_order() { + let mut expected_headers = Vec::new(); + let mut mock = mock("GET", "/"); + + // Add a large number of headers so getting the same order accidentally is unlikely. + for i in 0..100 { + let field = format!("x-custom-header-{}", i); + let value = "test"; + mock = mock.with_header(&field, value); + expected_headers.push(format!("{}: {}", field, value)); + } + + let _m = mock.create(); + + let (_, headers, _) = request("GET /", ""); + let custom_headers: Vec<_> = headers + .into_iter() + .filter(|header| header.starts_with("x-custom-header")) + .collect(); + + assert_eq!(custom_headers, expected_headers); +} + +#[test] +#[allow(deprecated)] +fn test_legacy_going_out_of_context_removes_mock() { + { + let _m = mock("GET", "/reset").create(); + + let (working_status_line, _, _) = request("GET /reset", ""); + assert_eq!("HTTP/1.1 200 OK\r\n", working_status_line); + } + + let (reset_status_line, _, _) = request("GET /reset", ""); + assert_eq!("HTTP/1.1 501 Not Implemented\r\n", reset_status_line); +} + +#[test] +#[allow(deprecated)] +fn test_legacy_going_out_of_context_doesnt_remove_other_mocks() { + let _m1 = mock("GET", "/long").create(); + + { + let _m2 = mock("GET", "/short").create(); + + let (short_status_line, _, _) = request("GET /short", ""); + assert_eq!("HTTP/1.1 200 OK\r\n", short_status_line); + } + + let (long_status_line, _, _) = request("GET /long", ""); + assert_eq!("HTTP/1.1 200 OK\r\n", long_status_line); +} + +#[test] +#[allow(deprecated)] +fn test_legacy_explicitly_calling_drop_removes_the_mock() { + let mock = mock("GET", "/").create(); + + let (status_line, _, _) = request("GET /", ""); + assert_eq!("HTTP/1.1 200 OK\r\n", status_line); + + mem::drop(mock); + + let (dropped_status_line, _, _) = request("GET /", ""); + assert_eq!("HTTP/1.1 501 Not Implemented\r\n", dropped_status_line); +} + +#[test] +#[allow(deprecated)] +fn test_legacy_regex_match_path() { + let _m1 = mock("GET", Matcher::Regex(r"^/a/\d{1}$".to_string())) + .with_body("aaa") + .create(); + let _m2 = mock("GET", Matcher::Regex(r"^/b/\d{1}$".to_string())) + .with_body("bbb") + .create(); + + let (_, _, body_a) = request("GET /a/1", ""); + assert_eq!("aaa", body_a); + + let (_, _, body_b) = request("GET /b/2", ""); + assert_eq!("bbb", body_b); + + let (status_line, _, _) = request("GET /a/11", ""); + assert_eq!("HTTP/1.1 501 Not Implemented\r\n", status_line); + + let (status_line, _, _) = request("GET /c/2", ""); + assert_eq!("HTTP/1.1 501 Not Implemented\r\n", status_line); +} + +#[test] +#[allow(deprecated)] +fn test_legacy_regex_match_header() { + let _m = mock("GET", "/") + .match_header( + "Authorization", + Matcher::Regex(r"^Bearer token\.\w+$".to_string()), + ) + .with_body("{}") + .create(); + + let (_, _, body_json) = request("GET /", "Authorization: Bearer token.payload\r\n"); + assert_eq!("{}", body_json); + + let (status_line, _, _) = request("GET /", "authorization: Beare none\r\n"); + assert_eq!("HTTP/1.1 501 Not Implemented\r\n", status_line); +} + +#[test] +#[allow(deprecated)] +fn test_legacy_any_of_match_header() { + let _m = mock("GET", "/") + .match_header( + "Via", + Matcher::AnyOf(vec![ + Matcher::Exact("one".into()), + Matcher::Exact("two".into()), + ]), + ) + .with_body("{}") + .create(); + + let (_, _, body_json) = request("GET /", "Via: one\r\n"); + assert_eq!("{}", body_json); + + let (_, _, body_json) = request("GET /", "Via: two\r\n"); + assert_eq!("{}", body_json); + + let (_, _, body_json) = request("GET /", "Via: one\r\nVia: two\r\n"); + assert_eq!("{}", body_json); + + let (status_line, _, _) = request("GET /", "Via: one\r\nVia: two\r\nVia: wrong\r\n"); + assert!(status_line.starts_with("HTTP/1.1 501 ")); + + let (status_line, _, _) = request("GET /", "Via: wrong\r\n"); + assert!(status_line.starts_with("HTTP/1.1 501 ")); +} + +#[test] +#[allow(deprecated)] +fn test_legacy_any_of_match_body() { + let _m = mock("GET", "/") + .match_body(Matcher::AnyOf(vec![ + Matcher::Regex("one".to_string()), + Matcher::Regex("two".to_string()), + ])) + .create(); + + let (status_line, _, _) = request_with_body("GET /", "", "one"); + assert!(status_line.starts_with("HTTP/1.1 200 ")); + + let (status_line, _, _) = request_with_body("GET /", "", "two"); + assert!(status_line.starts_with("HTTP/1.1 200 ")); + + let (status_line, _, _) = request_with_body("GET /", "", "one two"); + assert!(status_line.starts_with("HTTP/1.1 200 ")); + + let (status_line, _, _) = request_with_body("GET /", "", "three"); + assert!(status_line.starts_with("HTTP/1.1 501 ")); +} + +#[test] +#[allow(deprecated)] +fn test_legacy_any_of_missing_match_header() { + let _m = mock("GET", "/") + .match_header( + "Via", + Matcher::AnyOf(vec![Matcher::Exact("one".into()), Matcher::Missing]), + ) + .with_body("{}") + .create(); + + let (_, _, body_json) = request("GET /", "Via: one\r\n"); + assert_eq!("{}", body_json); + + let (_, _, body_json) = request("GET /", "Via: one\r\nVia: one\r\nVia: one\r\n"); + assert_eq!("{}", body_json); + + let (_, _, body_json) = request("GET /", "NotVia: one\r\n"); + assert_eq!("{}", body_json); + + let (status_line, _, _) = request("GET /", "Via: wrong\r\n"); + assert!(status_line.starts_with("HTTP/1.1 501 ")); + + let (status_line, _, _) = request("GET /", "Via: wrong\r\nVia: one\r\n"); + assert!(status_line.starts_with("HTTP/1.1 501 ")); + + let (status_line, _, _) = request("GET /", "Via: one\r\nVia: wrong\r\n"); + assert!(status_line.starts_with("HTTP/1.1 501 ")); +} + +#[test] +#[allow(deprecated)] +fn test_legacy_all_of_match_header() { + let _m = mock("GET", "/") + .match_header( + "Via", + Matcher::AllOf(vec![ + Matcher::Regex("one".into()), + Matcher::Regex("two".into()), + ]), + ) + .with_body("{}") + .create(); + + let (status_line, _, _) = request("GET /", "Via: one\r\n"); + assert!(status_line.starts_with("HTTP/1.1 501 ")); + + let (status_line, _, _) = request("GET /", "Via: two\r\n"); + assert!(status_line.starts_with("HTTP/1.1 501 ")); + + let (status_line, _, _) = request("GET /", "Via: one two\r\nVia: one two three\r\n"); + assert!(status_line.starts_with("HTTP/1.1 200 ")); + + let (status_line, _, _) = request("GET /", "Via: one\r\nVia: two\r\nVia: wrong\r\n"); + assert!(status_line.starts_with("HTTP/1.1 501 ")); + + let (status_line, _, _) = request("GET /", "Via: wrong\r\n"); + assert!(status_line.starts_with("HTTP/1.1 501 ")); +} + +#[test] +#[allow(deprecated)] +fn test_legacy_all_of_match_body() { + let _m = mock("GET", "/") + .match_body(Matcher::AllOf(vec![ + Matcher::Regex("one".to_string()), + Matcher::Regex("two".to_string()), + ])) + .create(); + + let (status_line, _, _) = request_with_body("GET /", "", "one"); + assert!(status_line.starts_with("HTTP/1.1 501 ")); + + let (status_line, _, _) = request_with_body("GET /", "", "two"); + assert!(status_line.starts_with("HTTP/1.1 501 ")); + + let (status_line, _, _) = request_with_body("GET /", "", "one two"); + assert!(status_line.starts_with("HTTP/1.1 200 ")); + + let (status_line, _, _) = request_with_body("GET /", "", "three"); + assert!(status_line.starts_with("HTTP/1.1 501 ")); +} + +#[test] +#[allow(deprecated)] +fn test_legacy_all_of_missing_match_header() { + let _m = mock("GET", "/") + .match_header("Via", Matcher::AllOf(vec![Matcher::Missing])) + .with_body("{}") + .create(); + + let (status_line, _, _) = request("GET /", "Via: one\r\n"); + assert!(status_line.starts_with("HTTP/1.1 501 ")); + + let (status_line, _, _) = request("GET /", "Via: one\r\nVia: one\r\nVia: one\r\n"); + assert!(status_line.starts_with("HTTP/1.1 501 ")); + + let (status_line, _, _) = request("GET /", "NotVia: one\r\n"); + assert!(status_line.starts_with("HTTP/1.1 200 ")); + + let (status_line, _, _) = request("GET /", "Via: wrong\r\n"); + assert!(status_line.starts_with("HTTP/1.1 501 ")); + + let (status_line, _, _) = request("GET /", "Via: wrong\r\nVia: one\r\n"); + assert!(status_line.starts_with("HTTP/1.1 501 ")); + + let (status_line, _, _) = request("GET /", "Via: one\r\nVia: wrong\r\n"); + assert!(status_line.starts_with("HTTP/1.1 501 ")); +} + +#[test] +#[allow(deprecated)] +fn test_legacy_large_utf8_body() { + let mock_body: String = rand::thread_rng() + .sample_iter(&Alphanumeric) + .map(char::from) + .take(3 * 1024) // Must be larger than the request read buffer + .map(char::from) + .collect(); + + let _m = mock("GET", "/").with_body(&mock_body).create(); + + let (_, _, body) = request("GET /", ""); + assert_eq!(mock_body, body); +} + +#[test] +#[allow(deprecated)] +fn test_legacy_body_from_file() { + let _m = mock("GET", "/") + .with_body_from_file("tests/files/simple.http") + .create(); + let (status_line, _, body) = request("GET /", ""); + assert_eq!("HTTP/1.1 200 OK\r\n", status_line); + assert_eq!("test body\n", body); +} + +#[test] +#[allow(deprecated)] +fn test_legacy_display_mock_matching_exact_path() { + let mock = mock("GET", "/hello"); + + assert_eq!("\r\nGET /hello\r\n", format!("{}", mock)); +} + +#[test] +#[allow(deprecated)] +fn test_legacy_display_mock_matching_regex_path() { + let mock = mock("GET", Matcher::Regex(r"^/hello/\d+$".to_string())); + + assert_eq!("\r\nGET ^/hello/\\d+$ (regex)\r\n", format!("{}", mock)); +} + +#[test] +#[allow(deprecated)] +fn test_legacy_display_mock_matching_any_path() { + let mock = mock("GET", Matcher::Any); + + assert_eq!("\r\nGET (any)\r\n", format!("{}", mock)); +} + +#[test] +#[allow(deprecated)] +fn test_legacy_display_mock_matching_exact_query() { + let mock = mock("GET", "/test?hello=world"); + + assert_eq!("\r\nGET /test?hello=world\r\n", format!("{}", mock)); +} + +#[test] +#[allow(deprecated)] +fn test_legacy_display_mock_matching_regex_query() { + let mock = mock("GET", "/test").match_query(Matcher::Regex("hello=world".to_string())); + + assert_eq!("\r\nGET /test?hello=world (regex)\r\n", format!("{}", mock)); +} + +#[test] +#[allow(deprecated)] +fn test_legacy_display_mock_matching_any_query() { + let mock = mock("GET", "/test").match_query(Matcher::Any); + + assert_eq!("\r\nGET /test?(any)\r\n", format!("{}", mock)); +} + +#[test] +#[allow(deprecated)] +fn test_legacy_display_mock_matching_exact_header() { + let mock = mock("GET", "/") + .match_header("content-type", "text") + .create(); + + assert_eq!("\r\nGET /\r\ncontent-type: text\r\n", format!("{}", mock)); +} + +#[test] +#[allow(deprecated)] +fn test_legacy_display_mock_matching_multiple_headers() { + let mock = mock("GET", "/") + .match_header("content-type", "text") + .match_header("content-length", Matcher::Regex(r"\d+".to_string())) + .match_header("authorization", Matcher::Any) + .match_header("x-request-id", Matcher::Missing) + .create(); + + assert_eq!("\r\nGET /\r\ncontent-type: text\r\ncontent-length: \\d+ (regex)\r\nauthorization: (any)\r\nx-request-id: (missing)\r\n", format!("{}", mock)); +} + +#[test] +#[allow(deprecated)] +fn test_legacy_display_mock_matching_exact_body() { + let mock = mock("POST", "/").match_body("hello").create(); + + assert_eq!("\r\nPOST /\r\nhello\r\n", format!("{}", mock)); +} + +#[test] +#[allow(deprecated)] +fn test_legacy_display_mock_matching_regex_body() { + let mock = mock("POST", "/") + .match_body(Matcher::Regex("hello".to_string())) + .create(); + + assert_eq!("\r\nPOST /\r\nhello\r\n", format!("{}", mock)); +} + +#[test] +#[allow(deprecated)] +fn test_legacy_display_mock_matching_any_body() { + let mock = mock("POST", "/").match_body(Matcher::Any).create(); + + assert_eq!("\r\nPOST /\r\n", format!("{}", mock)); +} + +#[test] +#[allow(deprecated)] +fn test_legacy_display_mock_matching_headers_and_body() { + let mock = mock("POST", "/") + .match_header("content-type", "text") + .match_body("hello") + .create(); + + assert_eq!( + "\r\nPOST /\r\ncontent-type: text\r\nhello\r\n", + format!("{}", mock) + ); +} + +#[test] +#[allow(deprecated)] +fn test_legacy_display_mock_matching_all_of_queries() { + let mock = mock("POST", "/") + .match_query(Matcher::AllOf(vec![ + Matcher::Exact("query1".to_string()), + Matcher::UrlEncoded("key".to_string(), "val".to_string()), + ])) + .create(); + + assert_eq!( + "\r\nPOST /?(query1, key=val (urlencoded)) (all of)\r\n", + format!("{}", mock) + ); +} + +#[test] +#[allow(deprecated)] +fn test_legacy_display_mock_matching_any_of_headers() { + let mock = mock("POST", "/") + .match_header( + "content-type", + Matcher::AnyOf(vec![ + Matcher::Exact("type1".to_string()), + Matcher::Regex("type2".to_string()), + ]), + ) + .create(); + + assert_eq!( + "\r\nPOST /\r\ncontent-type: (type1, type2 (regex)) (any of)\r\n", + format!("{}", mock) + ); +} +#[test] +#[allow(deprecated)] +fn test_legacy_assert_defaults_to_one_hit() { + let mock = mock("GET", "/hello").create(); + + request("GET /hello", ""); + + mock.assert(); +} + +#[test] +#[allow(deprecated)] +fn test_legacy_expect() { + let mock = mock("GET", "/hello").expect(3).create(); + + request("GET /hello", ""); + request("GET /hello", ""); + request("GET /hello", ""); + + mock.assert(); +} + +#[test] +#[allow(deprecated)] +fn test_legacy_expect_at_least_and_at_most() { + let mock = mock("GET", "/hello") + .expect_at_least(3) + .expect_at_most(6) + .create(); + + request("GET /hello", ""); + request("GET /hello", ""); + request("GET /hello", ""); + request("GET /hello", ""); + request("GET /hello", ""); + + mock.assert(); +} + +#[test] +#[allow(deprecated)] +fn test_legacy_expect_at_least() { + let mock = mock("GET", "/hello").expect_at_least(3).create(); + + request("GET /hello", ""); + request("GET /hello", ""); + request("GET /hello", ""); + + mock.assert(); +} + +#[test] +#[allow(deprecated)] +fn test_legacy_expect_at_least_more() { + let mock = mock("GET", "/hello").expect_at_least(3).create(); + + request("GET /hello", ""); + request("GET /hello", ""); + request("GET /hello", ""); + request("GET /hello", ""); + + mock.assert(); +} + +#[test] +#[allow(deprecated)] +fn test_legacy_expect_at_most_with_needed_requests() { + let mock = mock("GET", "/hello").expect_at_most(3).create(); + + request("GET /hello", ""); + request("GET /hello", ""); + request("GET /hello", ""); + + mock.assert(); +} + +#[test] +#[allow(deprecated)] +fn test_legacy_expect_at_most_with_few_requests() { + let mock = mock("GET", "/hello").expect_at_most(3).create(); + + request("GET /hello", ""); + request("GET /hello", ""); + + mock.assert(); +} + +#[test] +#[allow(deprecated)] +#[should_panic( + expected = "\n> Expected at least 3 request(s) to:\n\r\nGET /hello\r\n\n...but received 2\n" +)] +fn test_legacy_assert_panics_expect_at_least_with_too_few_requests() { + let mock = mock("GET", "/hello").expect_at_least(3).create(); + + request("GET /hello", ""); + request("GET /hello", ""); + + mock.assert(); +} + +#[test] +#[allow(deprecated)] +#[should_panic( + expected = "\n> Expected at most 3 request(s) to:\n\r\nGET /hello\r\n\n...but received 4\n" +)] +fn test_legacy_assert_panics_expect_at_most_with_too_many_requests() { + let mock = mock("GET", "/hello").expect_at_most(3).create(); + + request("GET /hello", ""); + request("GET /hello", ""); + request("GET /hello", ""); + request("GET /hello", ""); + + mock.assert(); +} + +#[test] +#[allow(deprecated)] +#[should_panic( + expected = "\n> Expected between 3 and 5 request(s) to:\n\r\nGET /hello\r\n\n...but received 2\n" +)] +fn test_legacy_assert_panics_expect_at_least_and_at_most_with_too_few_requests() { + let mock = mock("GET", "/hello") + .expect_at_least(3) + .expect_at_most(5) + .create(); + + request("GET /hello", ""); + request("GET /hello", ""); + + mock.assert(); +} + +#[test] +#[allow(deprecated)] +#[should_panic( + expected = "\n> Expected between 3 and 5 request(s) to:\n\r\nGET /hello\r\n\n...but received 6\n" +)] +fn test_legacy_assert_panics_expect_at_least_and_at_most_with_too_many_requests() { + let mock = mock("GET", "/hello") + .expect_at_least(3) + .expect_at_most(5) + .create(); + + request("GET /hello", ""); + request("GET /hello", ""); + request("GET /hello", ""); + request("GET /hello", ""); + request("GET /hello", ""); + request("GET /hello", ""); + + mock.assert(); +} + +#[test] +#[allow(deprecated)] +#[should_panic(expected = "\n> Expected 1 request(s) to:\n\r\nGET /hello\r\n\n...but received 0\n")] +fn test_legacy_assert_panics_if_no_request_was_performed() { + let mock = mock("GET", "/hello").create(); + + mock.assert(); +} + +#[test] +#[allow(deprecated)] +#[should_panic(expected = "\n> Expected 3 request(s) to:\n\r\nGET /hello\r\n\n...but received 2\n")] +fn test_legacy_assert_panics_with_too_few_requests() { + let mock = mock("GET", "/hello").expect(3).create(); + + request("GET /hello", ""); + request("GET /hello", ""); + + mock.assert(); +} + +#[test] +#[allow(deprecated)] +#[should_panic(expected = "\n> Expected 3 request(s) to:\n\r\nGET /hello\r\n\n...but received 4\n")] +fn test_legacy_assert_panics_with_too_many_requests() { + let mock = mock("GET", "/hello").expect(3).create(); + + request("GET /hello", ""); + request("GET /hello", ""); + request("GET /hello", ""); + request("GET /hello", ""); + + mock.assert(); +} + +#[test] +#[allow(deprecated)] +#[should_panic( + expected = "\n> Expected 1 request(s) to:\n\r\nGET /hello\r\n\n...but received 0\n\n> The last unmatched request was:\n\r\nGET /bye\r\n\n> Difference:\n\n\u{1b}[31mGET /hello\n\u{1b}[0m\u{1b}[32mGET\u{1b}[0m\u{1b}[32m \u{1b}[0m\u{1b}[42;37m/bye\u{1b}[0m\u{1b}[32m\n\u{1b}[0m\n\n" +)] +#[cfg(feature = "color")] +fn test_legacy_assert_with_last_unmatched_request() { + let mock = mock("GET", "/hello").create(); + + request("GET /bye", ""); + + mock.assert(); +} + +// Same test but without colors (for Appveyor) +#[test] +#[allow(deprecated)] +#[should_panic( + expected = "\n> Expected 1 request(s) to:\n\r\nGET /hello\r\n\n...but received 0\n\n> The last unmatched request was:\n\r\nGET /bye\r\n\n> Difference:\n\nGET /hello\nGET /bye\n\n\n" +)] +#[cfg(not(feature = "color"))] +fn test_legacy_assert_with_last_unmatched_request() { + let mock = mock("GET", "/hello").create(); + + request("GET /bye", ""); + + mock.assert(); +} + +#[test] +#[allow(deprecated)] +#[should_panic( + expected = "\n> Expected 1 request(s) to:\n\r\nGET /hello\r\n\n...but received 0\n\n> The last unmatched request was:\n\r\nGET /bye\r\nauthorization: 1234\r\naccept: text\r\n\n> Difference:\n\n\u{1b}[31mGET /hello\n\u{1b}[0m\u{1b}[32mGET\u{1b}[0m\u{1b}[32m \u{1b}[0m\u{1b}[42;37m/bye\u{1b}[0m\u{1b}[32m\n\u{1b}[0m\u{1b}[92mauthorization: 1234\n\u{1b}[0m\u{1b}[92maccept: text\n\u{1b}[0m\n\n" +)] +#[cfg(feature = "color")] +fn test_legacy_assert_with_last_unmatched_request_and_headers() { + let mock = mock("GET", "/hello").create(); + + request("GET /bye", "authorization: 1234\r\naccept: text\r\n"); + + mock.assert(); +} + +// Same test but without colors (for Appveyor) +#[test] +#[allow(deprecated)] +#[should_panic( + expected = "\n> Expected 1 request(s) to:\n\r\nGET /hello\r\n\n...but received 0\n\n> The last unmatched request was:\n\r\nGET /bye\r\nauthorization: 1234\r\naccept: text\r\n\n> Difference:\n\nGET /hello\nGET /bye\nauthorization: 1234\naccept: text\n\n\n" +)] +#[cfg(not(feature = "color"))] +fn test_legacy_assert_with_last_unmatched_request_and_headers() { + let mock = mock("GET", "/hello").create(); + + request("GET /bye", "authorization: 1234\r\naccept: text\r\n"); + + mock.assert(); +} + +#[test] +#[allow(deprecated)] +#[should_panic( + expected = "\n> Expected 1 request(s) to:\n\r\nGET /hello\r\n\n...but received 0\n\n> The last unmatched request was:\n\r\nPOST /bye\r\ncontent-length: 5\r\nhello\r\n\n" +)] +fn test_legacy_assert_with_last_unmatched_request_and_body() { + let mock = mock("GET", "/hello").create(); + + request_with_body("POST /bye", "", "hello"); + + mock.assert(); +} + +#[test] +#[allow(deprecated)] +fn test_legacy_request_from_thread() { + let mock = mock("GET", "/").create(); + + let process = thread::spawn(move || { + request("GET /", ""); + }); + + process.join().unwrap(); + + mock.assert(); +} + +#[test] +#[allow(deprecated)] +#[ignore] +// Can't work unless there's a way to apply LOCAL_TEST_MUTEX only to test threads and +// not to any of their sub-threads. +fn test_legacy_mock_from_inside_thread_does_not_lock_forever() { + let _mock_outside_thread = mock("GET", "/").with_body("outside").create(); + + let process = thread::spawn(move || { + let _mock_inside_thread = mock("GET", "/").with_body("inside").create(); + }); + + process.join().unwrap(); + + let (_, _, body) = request("GET /", ""); + + assert_eq!("outside", body); +} + +#[test] +#[allow(deprecated)] +fn test_legacy_head_request_with_overridden_content_length() { + let _mock = mock("HEAD", "/") + .with_header("content-length", "100") + .create(); + + let (_, headers, _) = request("HEAD /", ""); + + assert_eq!( + vec!["connection: close", "content-length: 100"], + headers[0..=1] + ); +} + +#[test] +#[allow(deprecated)] +fn test_legacy_propagate_protocol_to_response() { + let _mock = mock("GET", "/").create(); + + let stream = request_stream("1.0", "GET /", "", ""); + + let (status_line, _, _) = parse_stream(stream, true); + assert_eq!("HTTP/1.0 200 OK\r\n", status_line); +} + +#[test] +#[allow(deprecated)] +fn test_legacy_large_body_without_content_length() { + let body = "123".repeat(2048); + + let _mock = mock("POST", "/").match_body(body.as_str()).create(); + + let headers = format!("content-length: {}\r\n", body.len()); + let stream = request_stream("1.0", "POST /", &headers, &body); + + let (status_line, _, _) = parse_stream(stream, false); + assert_eq!("HTTP/1.0 200 OK\r\n", status_line); +} + +#[test] +#[allow(deprecated)] +fn test_legacy_transfer_encoding_chunked() { + let _mock = mock("POST", "/") + .match_body("Hello, chunked world!") + .create(); + + let body = "3\r\nHel\r\n5\r\nlo, c\r\nD\r\nhunked world!\r\n0\r\n\r\n"; + + let (status, _, _) = parse_stream( + request_stream("1.1", "POST /", "Transfer-Encoding: chunked\r\n", body), + false, + ); + + assert_eq!("HTTP/1.1 200 OK\r\n", status); +} + +#[test] +#[allow(deprecated)] +fn test_legacy_match_exact_query() { + let _m = mock("GET", "/hello") + .match_query(Matcher::Exact("number=one".to_string())) + .create(); + + let (status_line, _, _) = request("GET /hello?number=one", ""); + assert_eq!("HTTP/1.1 200 OK\r\n", status_line); + + let (status_line, _, _) = request("GET /hello?number=two", ""); + assert_eq!("HTTP/1.1 501 Not Implemented\r\n", status_line); +} + +#[test] +#[allow(deprecated)] +fn test_legacy_match_exact_query_via_path() { + let _m = mock("GET", "/hello?number=one").create(); + + let (status_line, _, _) = request("GET /hello?number=one", ""); + assert_eq!("HTTP/1.1 200 OK\r\n", status_line); + + let (status_line, _, _) = request("GET /hello?number=two", ""); + assert_eq!("HTTP/1.1 501 Not Implemented\r\n", status_line); +} + +#[test] +#[allow(deprecated)] +fn test_legacy_match_partial_query_by_regex() { + let _m = mock("GET", "/hello") + .match_query(Matcher::Regex("number=one".to_string())) + .create(); + + let (status_line, _, _) = request("GET /hello?something=else&number=one", ""); + assert_eq!("HTTP/1.1 200 OK\r\n", status_line); +} + +#[test] +#[allow(deprecated)] +fn test_legacy_match_partial_query_by_urlencoded() { + let _m = mock("GET", "/hello") + .match_query(Matcher::UrlEncoded("num ber".into(), "o ne".into())) + .create(); + + let (status_line, _, _) = request("GET /hello?something=else&num%20ber=o%20ne", ""); + assert_eq!("HTTP/1.1 200 OK\r\n", status_line); + + let (status_line, _, _) = request("GET /hello?something=else&number=one", ""); + assert_eq!("HTTP/1.1 501 Not Implemented\r\n", status_line); +} + +#[test] +#[allow(deprecated)] +fn test_legacy_match_partial_query_by_regex_all_of() { + let _m = mock("GET", "/hello") + .match_query(Matcher::AllOf(vec![ + Matcher::Regex("number=one".to_string()), + Matcher::Regex("hello=world".to_string()), + ])) + .create(); + + let (status_line, _, _) = request("GET /hello?hello=world&something=else&number=one", ""); + assert_eq!("HTTP/1.1 200 OK\r\n", status_line); + + let (status_line, _, _) = request("GET /hello?hello=world&something=else", ""); + assert_eq!("HTTP/1.1 501 Not Implemented\r\n", status_line); +} + +#[test] +#[allow(deprecated)] +fn test_legacy_match_partial_query_by_urlencoded_all_of() { + let _m = mock("GET", "/hello") + .match_query(Matcher::AllOf(vec![ + Matcher::UrlEncoded("num ber".into(), "o ne".into()), + Matcher::UrlEncoded("hello".into(), "world".into()), + ])) + .create(); + + let (status_line, _, _) = request("GET /hello?hello=world&something=else&num%20ber=o%20ne", ""); + assert_eq!("HTTP/1.1 200 OK\r\n", status_line); + + let (status_line, _, _) = request("GET /hello?hello=world&something=else", ""); + assert_eq!("HTTP/1.1 501 Not Implemented\r\n", status_line); +} + +#[test] +#[allow(deprecated)] +fn test_legacy_match_query_with_non_percent_url_escaping() { + let _m = mock("GET", "/hello") + .match_query(Matcher::AllOf(vec![ + Matcher::UrlEncoded("num ber".into(), "o ne".into()), + Matcher::UrlEncoded("hello".into(), "world".into()), + ])) + .create(); + + let (status_line, _, _) = request("GET /hello?hello=world&something=else&num+ber=o+ne", ""); + assert_eq!("HTTP/1.1 200 OK\r\n", status_line); +} + +#[test] +#[allow(deprecated)] +fn test_legacy_match_missing_query() { + let _m = mock("GET", "/hello").match_query(Matcher::Missing).create(); + + let (status_line, _, _) = request("GET /hello?", ""); + assert_eq!("HTTP/1.1 200 OK\r\n", status_line); + + let (status_line, _, _) = request("GET /hello?number=one", ""); + assert_eq!("HTTP/1.1 501 Not Implemented\r\n", status_line); +} + +#[test] +#[allow(deprecated)] +fn test_legacy_anyof_exact_path_and_query_matcher() { + let mock = mock( + "GET", + Matcher::AnyOf(vec![Matcher::Exact("/hello?world".to_string())]), + ) + .create(); + + let (status_line, _, _) = request("GET /hello?world", ""); + assert_eq!("HTTP/1.1 200 OK\r\n", status_line); + + mock.assert(); +} + +#[test] +#[allow(deprecated)] +fn test_legacy_default_headers() { + let _m = mock("GET", "/").create(); + + let (_, headers, _) = request("GET /", ""); + assert_eq!( + vec!["connection: close", "content-length: 0"], + headers[0..=1] + ); +} + +#[test] +#[allow(deprecated)] +fn test_legacy_missing_create_bad() { + testing_logger::setup(); + + let m = mock("GET", "/"); + drop(m); + + // Expecting one warning + testing_logger::validate(|captured_logs| { + let warnings = captured_logs + .iter() + .filter(|c| c.level == log::Level::Warn) + .collect::>(); + + assert_eq!(warnings.len(), 1); + assert_eq!( + warnings[0].body, + "Missing .create() call on mock \r\nGET /\r\n" + ); + assert_eq!(warnings[0].level, log::Level::Warn); + }); +} + +#[test] +#[allow(deprecated)] +fn test_legacy_missing_create_good() { + testing_logger::setup(); + + let m = mock("GET", "/").create(); + drop(m); + + // No warnings should occur + testing_logger::validate(|captured_logs| { + assert_eq!( + captured_logs + .iter() + .filter(|c| c.level == log::Level::Warn) + .count(), + 0 + ); + }); +} + +#[test] +#[allow(deprecated)] +fn test_legacy_same_endpoint_different_responses() { + let mock_200 = mock("GET", "/hello").with_status(200).create(); + let mock_404 = mock("GET", "/hello").with_status(404).create(); + let mock_500 = mock("GET", "/hello").with_status(500).create(); + + let response_200 = request("GET /hello", ""); + let response_404 = request("GET /hello", ""); + let response_500 = request("GET /hello", ""); + + mock_200.assert(); + mock_404.assert(); + mock_500.assert(); + + assert_eq!(response_200.0, "HTTP/1.1 200 OK\r\n"); + assert_eq!(response_404.0, "HTTP/1.1 404 Not Found\r\n"); + assert_eq!(response_500.0, "HTTP/1.1 500 Internal Server Error\r\n"); +} + +#[test] +#[allow(deprecated)] +fn test_legacy_same_endpoint_different_responses_last_one_forever() { + let _mock_200 = mock("GET", "/hello").with_status(200).create(); + let _mock_404 = mock("GET", "/hello").with_status(404).create(); + let _mock_500 = mock("GET", "/hello") + .expect_at_least(1) + .with_status(500) + .create(); + + let response_200 = request("GET /hello", ""); + let response_404 = request("GET /hello", ""); + let response_500_1 = request("GET /hello", ""); + let response_500_2 = request("GET /hello", ""); + let response_500_3 = request("GET /hello", ""); + + assert_eq!(response_200.0, "HTTP/1.1 200 OK\r\n"); + assert_eq!(response_404.0, "HTTP/1.1 404 Not Found\r\n"); + assert_eq!(response_500_1.0, "HTTP/1.1 500 Internal Server Error\r\n"); + assert_eq!(response_500_2.0, "HTTP/1.1 500 Internal Server Error\r\n"); + assert_eq!(response_500_3.0, "HTTP/1.1 500 Internal Server Error\r\n"); +} + +#[test] +#[allow(deprecated)] +fn test_legacy_matched_bool() { + let m = mock("GET", "/").create(); + + let (_, _, _) = request_with_body("GET /", "", ""); + m.assert(); + assert!(m.matched(), "matched method returns correctly"); + + let (_, _, _) = request_with_body("GET /", "", ""); + assert!(!m.matched(), "matched method returns correctly"); +} + +#[test] +#[allow(deprecated)] +fn test_legacy_invalid_header_field_name() { + let _m = mock("GET", "/").create(); + + let (uppercase_status_line, _, body) = request("GET /", "Bad Header: something\r\n"); + assert_eq!("HTTP/1.1 400 Bad Request\r\n", uppercase_status_line); + assert_eq!(body, ""); +} diff --git a/tests/lib.rs b/tests/lib.rs index 82b3012..99cb0f8 100644 --- a/tests/lib.rs +++ b/tests/lib.rs @@ -1,26 +1,32 @@ #[macro_use] extern crate serde_json; -use mockito::{mock, server_address, Matcher}; +use hyper::{body::Buf, client::conn, Body, Version}; +use mockito::{Matcher, Server}; use rand::distributions::Alphanumeric; use rand::Rng; +use std::fmt::Display; use std::fs; use std::io::{BufRead, BufReader, Read, Write}; use std::mem; -use std::net::{Shutdown, TcpStream}; +use std::net::TcpStream; use std::path::Path; use std::str::FromStr; +use std::sync::{Arc, Mutex}; use std::thread; type Binary = Vec; -fn request_stream>( +fn request_stream>( version: &str, + host: S, route: &str, headers: &str, body: StrOrBytes, ) -> TcpStream { - let mut stream = TcpStream::connect(server_address()).unwrap(); + let host = host.to_string(); + let mut stream = + TcpStream::connect(&host).unwrap_or_else(|_| panic!("couldn't connect to {}", &host)); let mut message: Binary = Vec::new(); for b in [route, " HTTP/", version, "\r\n", headers, "\r\n"] .join("") @@ -99,224 +105,318 @@ fn parse_stream(stream: TcpStream, skip_body: bool) -> (String, Vec, Bin (status_line, headers, body) } -fn binary_request>( +fn binary_request>( + host: S, route: &str, headers: &str, body: StrOrBytes, ) -> (String, Vec, Binary) { parse_stream( - request_stream("1.1", route, headers, body), + request_stream("1.1", host, route, headers, body), route.starts_with("HEAD"), ) } -fn request(route: &str, headers: &str) -> (String, Vec, String) { - let (status, headers, body) = binary_request(route, headers, ""); +fn request(host: S, route: &str, headers: &str) -> (String, Vec, String) { + let (status, headers, body) = binary_request(host, route, headers, ""); let parsed_body: String = std::str::from_utf8(body.as_slice()).unwrap().to_string(); (status, headers, parsed_body) } -fn request_with_body(route: &str, headers: &str, body: &str) -> (String, Vec, String) { +fn request_with_body( + host: S, + route: &str, + headers: &str, + body: &str, +) -> (String, Vec, String) { let headers = format!("{}content-length: {}\r\n", headers, body.len()); - let (status, headers, body) = binary_request(route, &headers, body); + let (status, headers, body) = binary_request(host, route, &headers, body); let parsed_body: String = std::str::from_utf8(body.as_slice()).unwrap().to_string(); (status, headers, parsed_body) } +async fn hyper_request( + host: &str, + version: &str, + method: &str, + uri: &str, + body: Option, +) -> (u16, Vec<(String, String)>, String) { + use tokio::net::TcpStream; + + let version = match version { + "1.0" => Version::HTTP_10, + "1.1" => Version::HTTP_11, + "2.0" => Version::HTTP_2, + _ => panic!("unrecognized version"), + }; + + let target_stream = TcpStream::connect(host) + .await + .map_err(|_err| -> Result { + Err(format!("couldn't connect to {}", host)) + }) + .unwrap(); + let (mut request_sender, connection) = + conn::Builder::new().handshake(target_stream).await.unwrap(); + tokio::task::spawn(async move { connection.await }); + + let body = body.map_or_else(Body::empty, Body::from); + let req = hyper::Request::builder() + .method(method) + .uri(uri) + .version(version) + .body(body) + .unwrap(); + + let mut response = request_sender.send_request(req).await.unwrap(); + + let status = response.status().as_u16(); + + let raw_body = response.body_mut(); + let mut buf = hyper::body::aggregate(raw_body).await.unwrap(); + let body_bytes = buf.copy_to_bytes(buf.remaining()).to_vec(); + let body = String::from_utf8_lossy(&body_bytes).to_string(); + + (status, vec![], body) +} + #[test] fn test_create_starts_the_server() { - let _m = mock("GET", "/").with_body("hello").create(); + let mut s = Server::new(); + let _m1 = s.mock("GET", "/").with_body("hello").create(); - let stream = TcpStream::connect(server_address()); + let stream = TcpStream::connect(s.host_with_port()); assert!(stream.is_ok()); } #[test] fn test_simple_route_mock() { - let _m = mock("GET", "/hello").with_body("world").create(); + let mut s = Server::new(); + let _m1 = s.mock("GET", "/hello").with_body("world").create(); - let (status_line, _, body) = request("GET /hello", ""); + let (status_line, _, body) = request(&s.host_with_port(), "GET /hello", ""); assert_eq!("HTTP/1.1 200 OK\r\n", status_line); assert_eq!("world", body); } #[test] fn test_two_route_mocks() { - let _m1 = mock("GET", "/a").with_body("aaa").create(); - let _m2 = mock("GET", "/b").with_body("bbb").create(); - - let (_, _, body_a) = request("GET /a", ""); + let mut s = Server::new(); + let _m1 = s.mock("GET", "/a").with_body("aaa").create(); + let _m2 = s.mock("GET", "/b").with_body("bbb").create(); + let (_, _, body_a) = request(&s.host_with_port(), "GET /a", ""); assert_eq!("aaa", body_a); - let (_, _, body_b) = request("GET /b", ""); + + let (_, _, body_b) = request(&s.host_with_port(), "GET /b", ""); assert_eq!("bbb", body_b); } #[test] fn test_no_match_returns_501() { - let _m = mock("GET", "/").with_body("matched").create(); + let mut s = Server::new(); + let _m1 = s.mock("GET", "/").with_body("matched").create(); - let (status_line, headers, _) = request("GET /nope", ""); - assert_eq!("HTTP/1.1 501 Mock Not Found\r\n", status_line); - assert_eq!(vec!["content-length: 0"], headers); + let (status_line, _headers, body) = request(&s.host_with_port(), "GET /nope", ""); + assert_eq!("HTTP/1.1 501 Not Implemented\r\n", status_line); + assert_eq!("", body); } #[test] fn test_match_header() { - let _m1 = mock("GET", "/") + let mut s = Server::new(); + let _m1 = s + .mock("GET", "/") .match_header("content-type", "application/json") .with_body("{}") .create(); - let _m2 = mock("GET", "/") + let _m2 = s + .mock("GET", "/") .match_header("content-type", "text/plain") .with_body("hello") .create(); - let (_, _, body_json) = request("GET /", "content-type: application/json\r\n"); + let (_, _, body_json) = request( + &s.host_with_port(), + "GET /", + "content-type: application/json\r\n", + ); assert_eq!("{}", body_json); - let (_, _, body_text) = request("GET /", "content-type: text/plain\r\n"); + let (_, _, body_text) = request(&s.host_with_port(), "GET /", "content-type: text/plain\r\n"); assert_eq!("hello", body_text); } #[test] fn test_match_header_is_case_insensitive_on_the_field_name() { - let _m = mock("GET", "/") + let mut s = Server::new(); + let _m = s + .mock("GET", "/") .match_header("content-type", "text/plain") .create(); - let (uppercase_status_line, _, _) = request("GET /", "Content-Type: text/plain\r\n"); + let (uppercase_status_line, _, _) = + request(&s.host_with_port(), "GET /", "Content-Type: text/plain\r\n"); assert_eq!("HTTP/1.1 200 OK\r\n", uppercase_status_line); - let (lowercase_status_line, _, _) = request("GET /", "content-type: text/plain\r\n"); + let (lowercase_status_line, _, _) = + request(&s.host_with_port(), "GET /", "content-type: text/plain\r\n"); assert_eq!("HTTP/1.1 200 OK\r\n", lowercase_status_line); } #[test] fn test_match_multiple_headers() { - let _m = mock("GET", "/") + let mut s = Server::new(); + let _m = s + .mock("GET", "/") .match_header("Content-Type", "text/plain") .match_header("Authorization", "secret") .with_body("matched") .create(); let (_, _, body_matching) = request( + &s.host_with_port(), "GET /", "content-type: text/plain\r\nauthorization: secret\r\n", ); assert_eq!("matched", body_matching); let (status_not_matching, _, _) = request( + &s.host_with_port(), "GET /", "content-type: text/plain\r\nauthorization: meh\r\n", ); - assert_eq!("HTTP/1.1 501 Mock Not Found\r\n", status_not_matching); + assert_eq!("HTTP/1.1 501 Not Implemented\r\n", status_not_matching); } #[test] fn test_match_header_any_matching() { - let _m = mock("GET", "/") + let mut s = Server::new(); + let _m = s + .mock("GET", "/") .match_header("Content-Type", Matcher::Any) .with_body("matched") .create(); - let (_, _, body) = request("GET /", "content-type: something\r\n"); + let (_, _, body) = request(&s.host_with_port(), "GET /", "content-type: something\r\n"); assert_eq!("matched", body); } #[test] fn test_match_header_any_not_matching() { - let _m = mock("GET", "/") + let mut s = Server::new(); + let _m = s + .mock("GET", "/") .match_header("Content-Type", Matcher::Any) .with_body("matched") .create(); - let (status, _, _) = request("GET /", ""); - assert_eq!("HTTP/1.1 501 Mock Not Found\r\n", status); + let (status, _, _) = request(&s.host_with_port(), "GET /", ""); + assert_eq!("HTTP/1.1 501 Not Implemented\r\n", status); } #[test] fn test_match_header_missing_matching() { - let _m = mock("GET", "/") + let mut s = Server::new(); + let _m = s + .mock("GET", "/") .match_header("Authorization", Matcher::Missing) .create(); - let (status, _, _) = request("GET /", ""); + let (status, _, _) = request(&s.host_with_port(), "GET /", ""); assert_eq!("HTTP/1.1 200 OK\r\n", status); } #[test] fn test_match_header_missing_not_matching() { - let _m = mock("GET", "/") + let mut s = Server::new(); + let _m = s + .mock("GET", "/") .match_header("Authorization", Matcher::Missing) .create(); - let (status, _, _) = request("GET /", "Authorization: something\r\n"); - assert_eq!("HTTP/1.1 501 Mock Not Found\r\n", status); + let (status, _, _) = request(&s.host_with_port(), "GET /", "Authorization: something\r\n"); + assert_eq!("HTTP/1.1 501 Not Implemented\r\n", status); } #[test] fn test_match_header_missing_not_matching_even_when_empty() { - let _m = mock("GET", "/") + let mut s = Server::new(); + let _m = s + .mock("GET", "/") .match_header("Authorization", Matcher::Missing) .create(); - let (status, _, _) = request("GET /", "Authorization:\r\n"); - assert_eq!("HTTP/1.1 501 Mock Not Found\r\n", status); + let (status, _, _) = request(&s.host_with_port(), "GET /", "Authorization:\r\n"); + assert_eq!("HTTP/1.1 501 Not Implemented\r\n", status); } #[test] fn test_match_multiple_header_conditions_matching() { - let _m = mock("GET", "/") + let mut s = Server::new(); + let _m = s + .mock("GET", "/") .match_header("Hello", "World") .match_header("Content-Type", Matcher::Any) .match_header("Authorization", Matcher::Missing) .create(); - let (status, _, _) = request("GET /", "Hello: World\r\nContent-Type: something\r\n"); + let (status, _, _) = request( + &s.host_with_port(), + "GET /", + "Hello: World\r\nContent-Type: something\r\n", + ); assert_eq!("HTTP/1.1 200 OK\r\n", status); } #[test] fn test_match_multiple_header_conditions_not_matching() { - let _m = mock("GET", "/") + let mut s = Server::new(); + let _m = s + .mock("GET", "/") .match_header("hello", "world") .match_header("Content-Type", Matcher::Any) .match_header("Authorization", Matcher::Missing) .create(); - let (status, _, _) = request("GET /", "Hello: World\r\n"); - assert_eq!("HTTP/1.1 501 Mock Not Found\r\n", status); + let (status, _, _) = request(&s.host_with_port(), "GET /", "Hello: World\r\n"); + assert_eq!("HTTP/1.1 501 Not Implemented\r\n", status); } #[test] fn test_match_any_body_by_default() { - let _m = mock("POST", "/").create(); + let mut s = Server::new(); + let _m = s.mock("POST", "/").create(); - let (status, _, _) = request_with_body("POST /", "", "hello"); + let (status, _, _) = request_with_body(&s.host_with_port(), "POST /", "", "hello"); assert_eq!("HTTP/1.1 200 OK\r\n", status); } #[test] fn test_match_body() { - let _m = mock("POST", "/").match_body("hello").create(); + let mut s = Server::new(); + let _m = s.mock("POST", "/").match_body("hello").create(); - let (status, _, _) = request_with_body("POST /", "", "hello"); + let (status, _, _) = request_with_body(&s.host_with_port(), "POST /", "", "hello"); assert_eq!("HTTP/1.1 200 OK\r\n", status); } #[test] fn test_match_body_not_matching() { - let _m = mock("POST", "/").match_body("hello").create(); + let mut s = Server::new(); + let _m = s.mock("POST", "/").match_body("hello").create(); - let (status, _, _) = request_with_body("POST /", "", "bye"); - assert_eq!("HTTP/1.1 501 Mock Not Found\r\n", status); + let (status, _, _) = request_with_body(&s.host_with_port(), "POST /", "", "bye"); + assert_eq!("HTTP/1.1 501 Not Implemented\r\n", status); } #[test] fn test_match_binary_body() { - let _m = mock("POST", "/") + let mut s = Server::new(); + let _m = s + .mock("POST", "/") .match_body(Path::new("./tests/files/test_payload.bin")) .create(); @@ -326,55 +426,80 @@ fn test_match_binary_body() { .read_to_end(&mut file_content) .unwrap(); let content_length_header = format!("Content-Length: {}\r\n", file_content.len()); - let (status, _, _) = binary_request("POST /", &content_length_header, file_content); + let (status, _, _) = binary_request( + &s.host_with_port(), + "POST /", + &content_length_header, + file_content, + ); assert_eq!("HTTP/1.1 200 OK\r\n", status); } #[test] fn test_does_not_match_binary_body() { - let _m = mock("POST", "/") + let mut s = Server::new(); + let _m = s + .mock("POST", "/") .match_body(Path::new("./tests/files/test_payload.bin")) .create(); let file_content: Binary = (0..1024).map(|_| rand::random::()).collect(); let content_length_header = format!("Content-Length: {}\r\n", file_content.len()); - let (status, _, _) = binary_request("POST /", &content_length_header, file_content); - assert_eq!("HTTP/1.1 501 Mock Not Found\r\n", status); + let (status, _, _) = binary_request( + &s.host_with_port(), + "POST /", + &content_length_header, + file_content, + ); + assert_eq!("HTTP/1.1 501 Not Implemented\r\n", status); } #[test] fn test_match_body_with_regex() { - let _m = mock("POST", "/") + let mut s = Server::new(); + let _m = s + .mock("POST", "/") .match_body(Matcher::Regex("hello".to_string())) .create(); - let (status, _, _) = request_with_body("POST /", "", "test hello test"); + let (status, _, _) = request_with_body(&s.host_with_port(), "POST /", "", "test hello test"); assert_eq!("HTTP/1.1 200 OK\r\n", status); } #[test] fn test_match_body_with_regex_not_matching() { - let _m = mock("POST", "/") + let mut s = Server::new(); + let _m = s + .mock("POST", "/") .match_body(Matcher::Regex("hello".to_string())) .create(); - let (status, _, _) = request_with_body("POST /", "", "bye"); - assert_eq!("HTTP/1.1 501 Mock Not Found\r\n", status); + let (status, _, _) = request_with_body(&s.host_with_port(), "POST /", "", "bye"); + assert_eq!("HTTP/1.1 501 Not Implemented\r\n", status); } #[test] fn test_match_body_with_json() { - let _m = mock("POST", "/") + let mut s = Server::new(); + let _m = s + .mock("POST", "/") .match_body(Matcher::Json(json!({"hello":"world", "foo": "bar"}))) .create(); - let (status, _, _) = request_with_body("POST /", "", r#"{"hello":"world", "foo": "bar"}"#); + let (status, _, _) = request_with_body( + &s.host_with_port(), + "POST /", + "", + r#"{"hello":"world", "foo": "bar"}"#, + ); assert_eq!("HTTP/1.1 200 OK\r\n", status); } #[test] fn test_match_body_with_more_headers_with_json() { - let _m = mock("POST", "/") + let mut s = Server::new(); + let _m = s + .mock("POST", "/") .match_body(Matcher::Json(json!({"hello":"world", "foo": "bar"}))) .create(); @@ -388,154 +513,209 @@ fn test_match_body_with_more_headers_with_json() { .collect::>() .concat(); - let (status, _, _) = - request_with_body("POST /", &headers, r#"{"hello":"world", "foo": "bar"}"#); + let (status, _, _) = request_with_body( + &s.host_with_port(), + "POST /", + &headers, + r#"{"hello":"world", "foo": "bar"}"#, + ); assert_eq!("HTTP/1.1 200 OK\r\n", status); } #[test] fn test_match_body_with_json_order() { - let _m = mock("POST", "/") + let mut s = Server::new(); + let _m = s + .mock("POST", "/") .match_body(Matcher::Json(json!({"foo": "bar", "hello": "world"}))) .create(); - let (status, _, _) = request_with_body("POST /", "", r#"{"hello":"world", "foo": "bar"}"#); + let (status, _, _) = request_with_body( + &s.host_with_port(), + "POST /", + "", + r#"{"hello":"world", "foo": "bar"}"#, + ); assert_eq!("HTTP/1.1 200 OK\r\n", status); } #[test] fn test_match_body_with_json_string() { - let _m = mock("POST", "/") + let mut s = Server::new(); + let _m = s + .mock("POST", "/") .match_body(Matcher::JsonString( "{\"hello\":\"world\", \"foo\": \"bar\"}".to_string(), )) .create(); - let (status, _, _) = request_with_body("POST /", "", r#"{"hello":"world", "foo": "bar"}"#); + let (status, _, _) = request_with_body( + &s.host_with_port(), + "POST /", + "", + r#"{"hello":"world", "foo": "bar"}"#, + ); assert_eq!("HTTP/1.1 200 OK\r\n", status); } #[test] fn test_match_body_with_json_string_order() { - let _m = mock("POST", "/") + let mut s = Server::new(); + let _m = s + .mock("POST", "/") .match_body(Matcher::JsonString( "{\"foo\": \"bar\", \"hello\": \"world\"}".to_string(), )) .create(); - let (status, _, _) = request_with_body("POST /", "", r#"{"hello":"world", "foo": "bar"}"#); + let (status, _, _) = request_with_body( + &s.host_with_port(), + "POST /", + "", + r#"{"hello":"world", "foo": "bar"}"#, + ); assert_eq!("HTTP/1.1 200 OK\r\n", status); } #[test] fn test_match_body_with_partial_json() { - let _m = mock("POST", "/") + let mut s = Server::new(); + let _m = s + .mock("POST", "/") .match_body(Matcher::PartialJson(json!({"hello":"world"}))) .create(); - let (status, _, _) = request_with_body("POST /", "", r#"{"hello":"world", "foo": "bar"}"#); + let (status, _, _) = request_with_body( + &s.host_with_port(), + "POST /", + "", + r#"{"hello":"world", "foo": "bar"}"#, + ); assert_eq!("HTTP/1.1 200 OK\r\n", status); } #[test] fn test_match_body_with_partial_json_and_extra_fields() { - let _m = mock("POST", "/") + let mut s = Server::new(); + let _m = s + .mock("POST", "/") .match_body(Matcher::PartialJson(json!({"hello":"world", "foo": "bar"}))) .create(); - let (status, _, _) = request_with_body("POST /", "", r#"{"hello":"world"}"#); - assert_eq!("HTTP/1.1 501 Mock Not Found\r\n", status); + let (status, _, _) = + request_with_body(&s.host_with_port(), "POST /", "", r#"{"hello":"world"}"#); + assert_eq!("HTTP/1.1 501 Not Implemented\r\n", status); } #[test] fn test_match_body_with_partial_json_string() { - let _m = mock("POST", "/") + let mut s = Server::new(); + let _m = s + .mock("POST", "/") .match_body(Matcher::PartialJsonString( "{\"hello\": \"world\"}".to_string(), )) .create(); - let (status, _, _) = request_with_body("POST /", "", r#"{"hello":"world", "foo": "bar"}"#); + let (status, _, _) = request_with_body( + &s.host_with_port(), + "POST /", + "", + r#"{"hello":"world", "foo": "bar"}"#, + ); assert_eq!("HTTP/1.1 200 OK\r\n", status); } #[test] fn test_match_body_with_partial_json_string_and_extra_fields() { - let _m = mock("POST", "/") + let mut s = Server::new(); + let _m = s + .mock("POST", "/") .match_body(Matcher::PartialJsonString( "{\"foo\": \"bar\", \"hello\": \"world\"}".to_string(), )) .create(); - let (status, _, _) = request_with_body("POST /", "", r#"{"hello":"world"}"#); - assert_eq!("HTTP/1.1 501 Mock Not Found\r\n", status); + let (status, _, _) = + request_with_body(&s.host_with_port(), "POST /", "", r#"{"hello":"world"}"#); + assert_eq!("HTTP/1.1 501 Not Implemented\r\n", status); } #[test] fn test_mock_with_status() { - let _m = mock("GET", "/").with_status(204).with_body("").create(); + let mut s = Server::new(); + let _m = s.mock("GET", "/").with_status(204).with_body("").create(); - let (status_line, _, _) = request("GET /", ""); + let (status_line, _, _) = request(&s.host_with_port(), "GET /", ""); assert_eq!("HTTP/1.1 204 No Content\r\n", status_line); } #[test] fn test_mock_with_custom_status() { - let _m = mock("GET", "/").with_status(333).with_body("").create(); + let mut s = Server::new(); + let _m = s.mock("GET", "/").with_status(499).with_body("").create(); - let (status_line, _, _) = request("GET /", ""); - assert_eq!("HTTP/1.1 333 Custom\r\n", status_line); + let (status_line, _, _) = request(&s.host_with_port(), "GET /", ""); + assert_eq!("HTTP/1.1 499 \r\n", status_line); } #[test] fn test_mock_with_body() { - let _m = mock("GET", "/").with_body("hello").create(); + let mut s = Server::new(); + let _m = s.mock("GET", "/").with_body("hello").create(); - let (_, _, body) = request("GET /", ""); + let (_, _, body) = request(&s.host_with_port(), "GET /", ""); assert_eq!("hello", body); } #[test] fn test_mock_with_fn_body() { - let _m = mock("GET", "/") + let mut s = Server::new(); + let _m = s + .mock("GET", "/") .with_body_from_fn(|w| { w.write_all(b"hel")?; w.write_all(b"lo") }) .create(); - let (_, _, body) = request("GET /", ""); + let (_, _, body) = request(&s.host_with_port(), "GET /", ""); assert_eq!("hello", body); } #[test] fn test_mock_with_header() { - let _m = mock("GET", "/") + let mut s = Server::new(); + let _m = s + .mock("GET", "/") .with_header("content-type", "application/json") .with_body("{}") .create(); - let (_, headers, _) = request("GET /", ""); + let (_, headers, _) = request(&s.host_with_port(), "GET /", ""); assert!(headers.contains(&"content-type: application/json".to_string())); } #[test] fn test_mock_with_multiple_headers() { - let _m = mock("GET", "/") + let mut s = Server::new(); + let _m = s + .mock("GET", "/") .with_header("content-type", "application/json") .with_header("x-api-key", "1234") .with_body("{}") .create(); - let (_, headers, _) = request("GET /", ""); + let (_, headers, _) = request(&s.host_with_port(), "GET /", ""); assert!(headers.contains(&"content-type: application/json".to_string())); assert!(headers.contains(&"x-api-key: 1234".to_string())); } #[test] fn test_mock_preserves_header_order() { + let mut s = Server::new(); let mut expected_headers = Vec::new(); - let mut mock = mock("GET", "/"); + let mut mock = s.mock("GET", "/"); // Add a large number of headers so getting the same order accidentally is unlikely. for i in 0..100 { @@ -547,7 +727,7 @@ fn test_mock_preserves_header_order() { let _m = mock.create(); - let (_, headers, _) = request("GET /", ""); + let (_, headers, _) = request(&s.host_with_port(), "GET /", ""); let custom_headers: Vec<_> = headers .into_iter() .filter(|header| header.starts_with("x-custom-header")) @@ -558,70 +738,79 @@ fn test_mock_preserves_header_order() { #[test] fn test_going_out_of_context_removes_mock() { + let mut s = Server::new(); { - let _m = mock("GET", "/reset").create(); + let _m = s.mock("GET", "/reset").create(); - let (working_status_line, _, _) = request("GET /reset", ""); + let (working_status_line, _, _) = request(&s.host_with_port(), "GET /reset", ""); assert_eq!("HTTP/1.1 200 OK\r\n", working_status_line); } - let (reset_status_line, _, _) = request("GET /reset", ""); - assert_eq!("HTTP/1.1 501 Mock Not Found\r\n", reset_status_line); + let (reset_status_line, _, _) = request(&s.host_with_port(), "GET /reset", ""); + assert_eq!("HTTP/1.1 501 Not Implemented\r\n", reset_status_line); } #[test] fn test_going_out_of_context_doesnt_remove_other_mocks() { - let _m1 = mock("GET", "/long").create(); + let mut s = Server::new(); + let _m1 = s.mock("GET", "/long").create(); { - let _m2 = mock("GET", "/short").create(); + let _m2 = s.mock("GET", "/short").create(); - let (short_status_line, _, _) = request("GET /short", ""); + let (short_status_line, _, _) = request(&s.host_with_port(), "GET /short", ""); assert_eq!("HTTP/1.1 200 OK\r\n", short_status_line); } - let (long_status_line, _, _) = request("GET /long", ""); + let (long_status_line, _, _) = request(&s.host_with_port(), "GET /long", ""); assert_eq!("HTTP/1.1 200 OK\r\n", long_status_line); } #[test] fn test_explicitly_calling_drop_removes_the_mock() { - let mock = mock("GET", "/").create(); + let mut s = Server::new(); + let host = s.host_with_port(); + let mock = s.mock("GET", "/").create(); - let (status_line, _, _) = request("GET /", ""); + let (status_line, _, _) = request(&host, "GET /", ""); assert_eq!("HTTP/1.1 200 OK\r\n", status_line); mem::drop(mock); - let (dropped_status_line, _, _) = request("GET /", ""); - assert_eq!("HTTP/1.1 501 Mock Not Found\r\n", dropped_status_line); + let (dropped_status_line, _, _) = request(&s.host_with_port(), "GET /", ""); + assert_eq!("HTTP/1.1 501 Not Implemented\r\n", dropped_status_line); } #[test] fn test_regex_match_path() { - let _m1 = mock("GET", Matcher::Regex(r"^/a/\d{1}$".to_string())) + let mut s = Server::new(); + let _m1 = s + .mock("GET", Matcher::Regex(r"^/a/\d{1}$".to_string())) .with_body("aaa") .create(); - let _m2 = mock("GET", Matcher::Regex(r"^/b/\d{1}$".to_string())) + let _m2 = s + .mock("GET", Matcher::Regex(r"^/b/\d{1}$".to_string())) .with_body("bbb") .create(); - let (_, _, body_a) = request("GET /a/1", ""); + let (_, _, body_a) = request(&s.host_with_port(), "GET /a/1", ""); assert_eq!("aaa", body_a); - let (_, _, body_b) = request("GET /b/2", ""); + let (_, _, body_b) = request(&s.host_with_port(), "GET /b/2", ""); assert_eq!("bbb", body_b); - let (status_line, _, _) = request("GET /a/11", ""); - assert_eq!("HTTP/1.1 501 Mock Not Found\r\n", status_line); + let (status_line, _, _) = request(&s.host_with_port(), "GET /a/11", ""); + assert_eq!("HTTP/1.1 501 Not Implemented\r\n", status_line); - let (status_line, _, _) = request("GET /c/2", ""); - assert_eq!("HTTP/1.1 501 Mock Not Found\r\n", status_line); + let (status_line, _, _) = request(&s.host_with_port(), "GET /c/2", ""); + assert_eq!("HTTP/1.1 501 Not Implemented\r\n", status_line); } #[test] fn test_regex_match_header() { - let _m = mock("GET", "/") + let mut s = Server::new(); + let _m = s + .mock("GET", "/") .match_header( "Authorization", Matcher::Regex(r"^Bearer token\.\w+$".to_string()), @@ -629,16 +818,26 @@ fn test_regex_match_header() { .with_body("{}") .create(); - let (_, _, body_json) = request("GET /", "Authorization: Bearer token.payload\r\n"); + let (_, _, body_json) = request( + &s.host_with_port(), + "GET /", + "Authorization: Bearer token.payload\r\n", + ); assert_eq!("{}", body_json); - let (status_line, _, _) = request("GET /", "authorization: Beare none\r\n"); - assert_eq!("HTTP/1.1 501 Mock Not Found\r\n", status_line); + let (status_line, _, _) = request( + &s.host_with_port(), + "GET /", + "authorization: Beare none\r\n", + ); + assert_eq!("HTTP/1.1 501 Not Implemented\r\n", status_line); } #[test] fn test_any_of_match_header() { - let _m = mock("GET", "/") + let mut s = Server::new(); + let _m = s + .mock("GET", "/") .match_header( "Via", Matcher::AnyOf(vec![ @@ -649,47 +848,55 @@ fn test_any_of_match_header() { .with_body("{}") .create(); - let (_, _, body_json) = request("GET /", "Via: one\r\n"); + let (_, _, body_json) = request(&s.host_with_port(), "GET /", "Via: one\r\n"); assert_eq!("{}", body_json); - let (_, _, body_json) = request("GET /", "Via: two\r\n"); + let (_, _, body_json) = request(&s.host_with_port(), "GET /", "Via: two\r\n"); assert_eq!("{}", body_json); - let (_, _, body_json) = request("GET /", "Via: one\r\nVia: two\r\n"); + let (_, _, body_json) = request(&s.host_with_port(), "GET /", "Via: one\r\nVia: two\r\n"); assert_eq!("{}", body_json); - let (status_line, _, _) = request("GET /", "Via: one\r\nVia: two\r\nVia: wrong\r\n"); + let (status_line, _, _) = request( + &s.host_with_port(), + "GET /", + "Via: one\r\nVia: two\r\nVia: wrong\r\n", + ); assert!(status_line.starts_with("HTTP/1.1 501 ")); - let (status_line, _, _) = request("GET /", "Via: wrong\r\n"); + let (status_line, _, _) = request(&s.host_with_port(), "GET /", "Via: wrong\r\n"); assert!(status_line.starts_with("HTTP/1.1 501 ")); } #[test] fn test_any_of_match_body() { - let _m = mock("GET", "/") + let mut s = Server::new(); + let _m = s + .mock("GET", "/") .match_body(Matcher::AnyOf(vec![ Matcher::Regex("one".to_string()), Matcher::Regex("two".to_string()), ])) .create(); - let (status_line, _, _) = request_with_body("GET /", "", "one"); + let (status_line, _, _) = request_with_body(&s.host_with_port(), "GET /", "", "one"); assert!(status_line.starts_with("HTTP/1.1 200 ")); - let (status_line, _, _) = request_with_body("GET /", "", "two"); + let (status_line, _, _) = request_with_body(&s.host_with_port(), "GET /", "", "two"); assert!(status_line.starts_with("HTTP/1.1 200 ")); - let (status_line, _, _) = request_with_body("GET /", "", "one two"); + let (status_line, _, _) = request_with_body(&s.host_with_port(), "GET /", "", "one two"); assert!(status_line.starts_with("HTTP/1.1 200 ")); - let (status_line, _, _) = request_with_body("GET /", "", "three"); + let (status_line, _, _) = request_with_body(&s.host_with_port(), "GET /", "", "three"); assert!(status_line.starts_with("HTTP/1.1 501 ")); } #[test] fn test_any_of_missing_match_header() { - let _m = mock("GET", "/") + let mut s = Server::new(); + let _m = s + .mock("GET", "/") .match_header( "Via", Matcher::AnyOf(vec![Matcher::Exact("one".into()), Matcher::Missing]), @@ -697,28 +904,34 @@ fn test_any_of_missing_match_header() { .with_body("{}") .create(); - let (_, _, body_json) = request("GET /", "Via: one\r\n"); + let (_, _, body_json) = request(&s.host_with_port(), "GET /", "Via: one\r\n"); assert_eq!("{}", body_json); - let (_, _, body_json) = request("GET /", "Via: one\r\nVia: one\r\nVia: one\r\n"); + let (_, _, body_json) = request( + &s.host_with_port(), + "GET /", + "Via: one\r\nVia: one\r\nVia: one\r\n", + ); assert_eq!("{}", body_json); - let (_, _, body_json) = request("GET /", "NotVia: one\r\n"); + let (_, _, body_json) = request(&s.host_with_port(), "GET /", "NotVia: one\r\n"); assert_eq!("{}", body_json); - let (status_line, _, _) = request("GET /", "Via: wrong\r\n"); + let (status_line, _, _) = request(&s.host_with_port(), "GET /", "Via: wrong\r\n"); assert!(status_line.starts_with("HTTP/1.1 501 ")); - let (status_line, _, _) = request("GET /", "Via: wrong\r\nVia: one\r\n"); + let (status_line, _, _) = request(&s.host_with_port(), "GET /", "Via: wrong\r\nVia: one\r\n"); assert!(status_line.starts_with("HTTP/1.1 501 ")); - let (status_line, _, _) = request("GET /", "Via: one\r\nVia: wrong\r\n"); + let (status_line, _, _) = request(&s.host_with_port(), "GET /", "Via: one\r\nVia: wrong\r\n"); assert!(status_line.starts_with("HTTP/1.1 501 ")); } #[test] fn test_all_of_match_header() { - let _m = mock("GET", "/") + let mut s = Server::new(); + let _m = s + .mock("GET", "/") .match_header( "Via", Matcher::AllOf(vec![ @@ -729,72 +942,89 @@ fn test_all_of_match_header() { .with_body("{}") .create(); - let (status_line, _, _) = request("GET /", "Via: one\r\n"); + let (status_line, _, _) = request(&s.host_with_port(), "GET /", "Via: one\r\n"); assert!(status_line.starts_with("HTTP/1.1 501 ")); - let (status_line, _, _) = request("GET /", "Via: two\r\n"); + let (status_line, _, _) = request(&s.host_with_port(), "GET /", "Via: two\r\n"); assert!(status_line.starts_with("HTTP/1.1 501 ")); - let (status_line, _, _) = request("GET /", "Via: one two\r\nVia: one two three\r\n"); + let (status_line, _, _) = request( + &s.host_with_port(), + "GET /", + "Via: one two\r\nVia: one two three\r\n", + ); assert!(status_line.starts_with("HTTP/1.1 200 ")); - let (status_line, _, _) = request("GET /", "Via: one\r\nVia: two\r\nVia: wrong\r\n"); + let (status_line, _, _) = request( + &s.host_with_port(), + "GET /", + "Via: one\r\nVia: two\r\nVia: wrong\r\n", + ); assert!(status_line.starts_with("HTTP/1.1 501 ")); - let (status_line, _, _) = request("GET /", "Via: wrong\r\n"); + let (status_line, _, _) = request(&s.host_with_port(), "GET /", "Via: wrong\r\n"); assert!(status_line.starts_with("HTTP/1.1 501 ")); } #[test] fn test_all_of_match_body() { - let _m = mock("GET", "/") + let mut s = Server::new(); + let _m = s + .mock("GET", "/") .match_body(Matcher::AllOf(vec![ Matcher::Regex("one".to_string()), Matcher::Regex("two".to_string()), ])) .create(); - let (status_line, _, _) = request_with_body("GET /", "", "one"); + let (status_line, _, _) = request_with_body(&s.host_with_port(), "GET /", "", "one"); assert!(status_line.starts_with("HTTP/1.1 501 ")); - let (status_line, _, _) = request_with_body("GET /", "", "two"); + let (status_line, _, _) = request_with_body(&s.host_with_port(), "GET /", "", "two"); assert!(status_line.starts_with("HTTP/1.1 501 ")); - let (status_line, _, _) = request_with_body("GET /", "", "one two"); + let (status_line, _, _) = request_with_body(&s.host_with_port(), "GET /", "", "one two"); assert!(status_line.starts_with("HTTP/1.1 200 ")); - let (status_line, _, _) = request_with_body("GET /", "", "three"); + let (status_line, _, _) = request_with_body(&s.host_with_port(), "GET /", "", "three"); assert!(status_line.starts_with("HTTP/1.1 501 ")); } #[test] fn test_all_of_missing_match_header() { - let _m = mock("GET", "/") + let mut s = Server::new(); + let _m = s + .mock("GET", "/") .match_header("Via", Matcher::AllOf(vec![Matcher::Missing])) .with_body("{}") .create(); - let (status_line, _, _) = request("GET /", "Via: one\r\n"); + let (status_line, _, _) = request(&s.host_with_port(), "GET /", "Via: one\r\n"); assert!(status_line.starts_with("HTTP/1.1 501 ")); - let (status_line, _, _) = request("GET /", "Via: one\r\nVia: one\r\nVia: one\r\n"); + let (status_line, _, _) = request( + &s.host_with_port(), + "GET /", + "Via: one\r\nVia: one\r\nVia: one\r\n", + ); assert!(status_line.starts_with("HTTP/1.1 501 ")); - let (status_line, _, _) = request("GET /", "NotVia: one\r\n"); + let (status_line, _, _) = request(&s.host_with_port(), "GET /", "NotVia: one\r\n"); assert!(status_line.starts_with("HTTP/1.1 200 ")); - let (status_line, _, _) = request("GET /", "Via: wrong\r\n"); + let (status_line, _, _) = request(&s.host_with_port(), "GET /", "Via: wrong\r\n"); assert!(status_line.starts_with("HTTP/1.1 501 ")); - let (status_line, _, _) = request("GET /", "Via: wrong\r\nVia: one\r\n"); + let (status_line, _, _) = request(&s.host_with_port(), "GET /", "Via: wrong\r\nVia: one\r\n"); assert!(status_line.starts_with("HTTP/1.1 501 ")); - let (status_line, _, _) = request("GET /", "Via: one\r\nVia: wrong\r\n"); + let (status_line, _, _) = request(&s.host_with_port(), "GET /", "Via: one\r\nVia: wrong\r\n"); assert!(status_line.starts_with("HTTP/1.1 501 ")); } #[test] fn test_large_utf8_body() { + let mut s = Server::new(); let mock_body: String = rand::thread_rng() .sample_iter(&Alphanumeric) .map(char::from) @@ -802,67 +1032,79 @@ fn test_large_utf8_body() { .map(char::from) .collect(); - let _m = mock("GET", "/").with_body(&mock_body).create(); + let _m = s.mock("GET", "/").with_body(&mock_body).create(); - let (_, _, body) = request("GET /", ""); + let (_, _, body) = request(&s.host_with_port(), "GET /", ""); assert_eq!(mock_body, body); } #[test] fn test_body_from_file() { - let _m = mock("GET", "/") + let mut s = Server::new(); + let _m = s + .mock("GET", "/") .with_body_from_file("tests/files/simple.http") .create(); - let (status_line, _, body) = request("GET /", ""); + let (status_line, _, body) = request(&s.host_with_port(), "GET /", ""); assert_eq!("HTTP/1.1 200 OK\r\n", status_line); assert_eq!("test body\n", body); } #[test] fn test_display_mock_matching_exact_path() { - let mock = mock("GET", "/hello"); + let mut s = Server::new(); + let mock = s.mock("GET", "/hello"); assert_eq!("\r\nGET /hello\r\n", format!("{}", mock)); } #[test] fn test_display_mock_matching_regex_path() { - let mock = mock("GET", Matcher::Regex(r"^/hello/\d+$".to_string())); + let mut s = Server::new(); + let mock = s.mock("GET", Matcher::Regex(r"^/hello/\d+$".to_string())); assert_eq!("\r\nGET ^/hello/\\d+$ (regex)\r\n", format!("{}", mock)); } #[test] fn test_display_mock_matching_any_path() { - let mock = mock("GET", Matcher::Any); + let mut s = Server::new(); + let mock = s.mock("GET", Matcher::Any); assert_eq!("\r\nGET (any)\r\n", format!("{}", mock)); } #[test] fn test_display_mock_matching_exact_query() { - let mock = mock("GET", "/test?hello=world"); + let mut s = Server::new(); + let mock = s.mock("GET", "/test?hello=world"); assert_eq!("\r\nGET /test?hello=world\r\n", format!("{}", mock)); } #[test] fn test_display_mock_matching_regex_query() { - let mock = mock("GET", "/test").match_query(Matcher::Regex("hello=world".to_string())); + let mut s = Server::new(); + let mock = s + .mock("GET", "/test") + .match_query(Matcher::Regex("hello=world".to_string())); assert_eq!("\r\nGET /test?hello=world (regex)\r\n", format!("{}", mock)); } #[test] fn test_display_mock_matching_any_query() { - let mock = mock("GET", "/test").match_query(Matcher::Any); + let mut s = Server::new(); + let mock = s.mock("GET", "/test").match_query(Matcher::Any); assert_eq!("\r\nGET /test?(any)\r\n", format!("{}", mock)); } #[test] fn test_display_mock_matching_exact_header() { - let mock = mock("GET", "/") + let mut s = Server::new(); + let mock = s + .mock("GET", "/") .match_header("content-type", "text") .create(); @@ -871,7 +1113,9 @@ fn test_display_mock_matching_exact_header() { #[test] fn test_display_mock_matching_multiple_headers() { - let mock = mock("GET", "/") + let mut s = Server::new(); + let mock = s + .mock("GET", "/") .match_header("content-type", "text") .match_header("content-length", Matcher::Regex(r"\d+".to_string())) .match_header("authorization", Matcher::Any) @@ -883,14 +1127,17 @@ fn test_display_mock_matching_multiple_headers() { #[test] fn test_display_mock_matching_exact_body() { - let mock = mock("POST", "/").match_body("hello").create(); + let mut s = Server::new(); + let mock = s.mock("POST", "/").match_body("hello").create(); assert_eq!("\r\nPOST /\r\nhello\r\n", format!("{}", mock)); } #[test] fn test_display_mock_matching_regex_body() { - let mock = mock("POST", "/") + let mut s = Server::new(); + let mock = s + .mock("POST", "/") .match_body(Matcher::Regex("hello".to_string())) .create(); @@ -899,14 +1146,17 @@ fn test_display_mock_matching_regex_body() { #[test] fn test_display_mock_matching_any_body() { - let mock = mock("POST", "/").match_body(Matcher::Any).create(); + let mut s = Server::new(); + let mock = s.mock("POST", "/").match_body(Matcher::Any).create(); assert_eq!("\r\nPOST /\r\n", format!("{}", mock)); } #[test] fn test_display_mock_matching_headers_and_body() { - let mock = mock("POST", "/") + let mut s = Server::new(); + let mock = s + .mock("POST", "/") .match_header("content-type", "text") .match_body("hello") .create(); @@ -919,7 +1169,9 @@ fn test_display_mock_matching_headers_and_body() { #[test] fn test_display_mock_matching_all_of_queries() { - let mock = mock("POST", "/") + let mut s = Server::new(); + let mock = s + .mock("POST", "/") .match_query(Matcher::AllOf(vec![ Matcher::Exact("query1".to_string()), Matcher::UrlEncoded("key".to_string(), "val".to_string()), @@ -934,7 +1186,9 @@ fn test_display_mock_matching_all_of_queries() { #[test] fn test_display_mock_matching_any_of_headers() { - let mock = mock("POST", "/") + let mut s = Server::new(); + let mock = s + .mock("POST", "/") .match_header( "content-type", Matcher::AnyOf(vec![ @@ -949,82 +1203,98 @@ fn test_display_mock_matching_any_of_headers() { format!("{}", mock) ); } + #[test] fn test_assert_defaults_to_one_hit() { - let mock = mock("GET", "/hello").create(); + let mut s = Server::new(); + let host = s.host_with_port(); + let mock = s.mock("GET", "/hello").create(); - request("GET /hello", ""); + request(&host, "GET /hello", ""); mock.assert(); } #[test] fn test_expect() { - let mock = mock("GET", "/hello").expect(3).create(); + let mut s = Server::new(); + let host = s.host_with_port(); + let mock = s.mock("GET", "/hello").expect(3).create(); - request("GET /hello", ""); - request("GET /hello", ""); - request("GET /hello", ""); + request(&host, "GET /hello", ""); + request(&host, "GET /hello", ""); + request(&host, "GET /hello", ""); mock.assert(); } #[test] fn test_expect_at_least_and_at_most() { - let mock = mock("GET", "/hello") + let mut s = Server::new(); + let host = s.host_with_port(); + let mock = s + .mock("GET", "/hello") .expect_at_least(3) .expect_at_most(6) .create(); - request("GET /hello", ""); - request("GET /hello", ""); - request("GET /hello", ""); - request("GET /hello", ""); - request("GET /hello", ""); + request(&host, "GET /hello", ""); + request(&host, "GET /hello", ""); + request(&host, "GET /hello", ""); + request(&host, "GET /hello", ""); + request(&host, "GET /hello", ""); mock.assert(); } #[test] fn test_expect_at_least() { - let mock = mock("GET", "/hello").expect_at_least(3).create(); + let mut s = Server::new(); + let host = s.host_with_port(); + let mock = s.mock("GET", "/hello").expect_at_least(3).create(); - request("GET /hello", ""); - request("GET /hello", ""); - request("GET /hello", ""); + request(&host, "GET /hello", ""); + request(&host, "GET /hello", ""); + request(&host, "GET /hello", ""); mock.assert(); } #[test] fn test_expect_at_least_more() { - let mock = mock("GET", "/hello").expect_at_least(3).create(); + let mut s = Server::new(); + let host = s.host_with_port(); + let mock = s.mock("GET", "/hello").expect_at_least(3).create(); - request("GET /hello", ""); - request("GET /hello", ""); - request("GET /hello", ""); - request("GET /hello", ""); + request(&host, "GET /hello", ""); + request(&host, "GET /hello", ""); + request(&host, "GET /hello", ""); + request(&host, "GET /hello", ""); mock.assert(); } #[test] fn test_expect_at_most_with_needed_requests() { - let mock = mock("GET", "/hello").expect_at_most(3).create(); + let mut s = Server::new(); + let host = s.host_with_port(); + let mock = s.mock("GET", "/hello").expect_at_most(3).create(); - request("GET /hello", ""); - request("GET /hello", ""); - request("GET /hello", ""); + request(&host, "GET /hello", ""); + request(&host, "GET /hello", ""); + request(&host, "GET /hello", ""); mock.assert(); } #[test] fn test_expect_at_most_with_few_requests() { - let mock = mock("GET", "/hello").expect_at_most(3).create(); + let mut s = Server::new(); + let host = s.host_with_port(); + let mock = s.mock("GET", "/hello").expect_at_most(3).create(); - request("GET /hello", ""); - request("GET /hello", ""); + request(&host, "GET /hello", ""); + request(&host, "GET /hello", ""); mock.assert(); } @@ -1034,10 +1304,12 @@ fn test_expect_at_most_with_few_requests() { expected = "\n> Expected at least 3 request(s) to:\n\r\nGET /hello\r\n\n...but received 2\n" )] fn test_assert_panics_expect_at_least_with_too_few_requests() { - let mock = mock("GET", "/hello").expect_at_least(3).create(); + let mut s = Server::new(); + let host = s.host_with_port(); + let mock = s.mock("GET", "/hello").expect_at_least(3).create(); - request("GET /hello", ""); - request("GET /hello", ""); + request(&host, "GET /hello", ""); + request(&host, "GET /hello", ""); mock.assert(); } @@ -1047,12 +1319,14 @@ fn test_assert_panics_expect_at_least_with_too_few_requests() { expected = "\n> Expected at most 3 request(s) to:\n\r\nGET /hello\r\n\n...but received 4\n" )] fn test_assert_panics_expect_at_most_with_too_many_requests() { - let mock = mock("GET", "/hello").expect_at_most(3).create(); + let mut s = Server::new(); + let host = s.host_with_port(); + let mock = s.mock("GET", "/hello").expect_at_most(3).create(); - request("GET /hello", ""); - request("GET /hello", ""); - request("GET /hello", ""); - request("GET /hello", ""); + request(&host, "GET /hello", ""); + request(&host, "GET /hello", ""); + request(&host, "GET /hello", ""); + request(&host, "GET /hello", ""); mock.assert(); } @@ -1062,13 +1336,16 @@ fn test_assert_panics_expect_at_most_with_too_many_requests() { expected = "\n> Expected between 3 and 5 request(s) to:\n\r\nGET /hello\r\n\n...but received 2\n" )] fn test_assert_panics_expect_at_least_and_at_most_with_too_few_requests() { - let mock = mock("GET", "/hello") + let mut s = Server::new(); + let host = s.host_with_port(); + let mock = s + .mock("GET", "/hello") .expect_at_least(3) .expect_at_most(5) .create(); - request("GET /hello", ""); - request("GET /hello", ""); + request(&host, "GET /hello", ""); + request(&host, "GET /hello", ""); mock.assert(); } @@ -1078,17 +1355,20 @@ fn test_assert_panics_expect_at_least_and_at_most_with_too_few_requests() { expected = "\n> Expected between 3 and 5 request(s) to:\n\r\nGET /hello\r\n\n...but received 6\n" )] fn test_assert_panics_expect_at_least_and_at_most_with_too_many_requests() { - let mock = mock("GET", "/hello") + let mut s = Server::new(); + let host = s.host_with_port(); + let mock = s + .mock("GET", "/hello") .expect_at_least(3) .expect_at_most(5) .create(); - request("GET /hello", ""); - request("GET /hello", ""); - request("GET /hello", ""); - request("GET /hello", ""); - request("GET /hello", ""); - request("GET /hello", ""); + request(&host, "GET /hello", ""); + request(&host, "GET /hello", ""); + request(&host, "GET /hello", ""); + request(&host, "GET /hello", ""); + request(&host, "GET /hello", ""); + request(&host, "GET /hello", ""); mock.assert(); } @@ -1096,7 +1376,8 @@ fn test_assert_panics_expect_at_least_and_at_most_with_too_many_requests() { #[test] #[should_panic(expected = "\n> Expected 1 request(s) to:\n\r\nGET /hello\r\n\n...but received 0\n")] fn test_assert_panics_if_no_request_was_performed() { - let mock = mock("GET", "/hello").create(); + let mut s = Server::new(); + let mock = s.mock("GET", "/hello").create(); mock.assert(); } @@ -1104,10 +1385,12 @@ fn test_assert_panics_if_no_request_was_performed() { #[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() { - let mock = mock("GET", "/hello").expect(3).create(); + let mut s = Server::new(); + let host = s.host_with_port(); + let mock = s.mock("GET", "/hello").expect(3).create(); - request("GET /hello", ""); - request("GET /hello", ""); + request(&host, "GET /hello", ""); + request(&host, "GET /hello", ""); mock.assert(); } @@ -1115,12 +1398,14 @@ fn test_assert_panics_with_too_few_requests() { #[test] #[should_panic(expected = "\n> Expected 3 request(s) to:\n\r\nGET /hello\r\n\n...but received 4\n")] fn test_assert_panics_with_too_many_requests() { - let mock = mock("GET", "/hello").expect(3).create(); + let mut s = Server::new(); + let host = s.host_with_port(); + let mock = s.mock("GET", "/hello").expect(3).create(); - request("GET /hello", ""); - request("GET /hello", ""); - request("GET /hello", ""); - request("GET /hello", ""); + request(&host, "GET /hello", ""); + request(&host, "GET /hello", ""); + request(&host, "GET /hello", ""); + request(&host, "GET /hello", ""); mock.assert(); } @@ -1131,9 +1416,11 @@ fn test_assert_panics_with_too_many_requests() { )] #[cfg(feature = "color")] fn test_assert_with_last_unmatched_request() { - let mock = mock("GET", "/hello").create(); + let mut s = Server::new(); + let host = s.host_with_port(); + let mock = s.mock("GET", "/hello").create(); - request("GET /bye", ""); + request(&host, "GET /bye", ""); mock.assert(); } @@ -1145,9 +1432,11 @@ fn test_assert_with_last_unmatched_request() { )] #[cfg(not(feature = "color"))] fn test_assert_with_last_unmatched_request() { - let mock = mock("GET", "/hello").create(); + let mut s = Server::new(); + let host = s.host_with_port(); + let mock = s.mock("GET", "/hello").create(); - request("GET /bye", ""); + request(&host, "GET /bye", ""); mock.assert(); } @@ -1158,9 +1447,11 @@ fn test_assert_with_last_unmatched_request() { )] #[cfg(feature = "color")] fn test_assert_with_last_unmatched_request_and_headers() { - let mock = mock("GET", "/hello").create(); + let mut s = Server::new(); + let host = s.host_with_port(); + let mock = s.mock("GET", "/hello").create(); - request("GET /bye", "authorization: 1234\r\naccept: text\r\n"); + request(&host, "GET /bye", "authorization: 1234\r\naccept: text\r\n"); mock.assert(); } @@ -1172,9 +1463,11 @@ fn test_assert_with_last_unmatched_request_and_headers() { )] #[cfg(not(feature = "color"))] fn test_assert_with_last_unmatched_request_and_headers() { - let mock = mock("GET", "/hello").create(); + let mut s = Server::new(); + let host = s.host_with_port(); + let mock = s.mock("GET", "/hello").create(); - request("GET /bye", "authorization: 1234\r\naccept: text\r\n"); + request(&host, "GET /bye", "authorization: 1234\r\naccept: text\r\n"); mock.assert(); } @@ -1184,19 +1477,23 @@ fn test_assert_with_last_unmatched_request_and_headers() { expected = "\n> Expected 1 request(s) to:\n\r\nGET /hello\r\n\n...but received 0\n\n> The last unmatched request was:\n\r\nPOST /bye\r\ncontent-length: 5\r\nhello\r\n\n" )] fn test_assert_with_last_unmatched_request_and_body() { - let mock = mock("GET", "/hello").create(); + let mut s = Server::new(); + let host = s.host_with_port(); + let mock = s.mock("GET", "/hello").create(); - request_with_body("POST /bye", "", "hello"); + request_with_body(&host, "POST /bye", "", "hello"); mock.assert(); } #[test] fn test_request_from_thread() { - let mock = mock("GET", "/").create(); + let mut s = Server::new(); + let host = s.host_with_port(); + let mock = s.mock("GET", "/").create(); let process = thread::spawn(move || { - request("GET /", ""); + request(&host, "GET /", ""); }); process.join().unwrap(); @@ -1206,39 +1503,48 @@ fn test_request_from_thread() { #[test] #[ignore] -// Can't work unless there's a way to apply LOCAL_TEST_MUTEX only to test threads and -// not to any of their sub-threads. fn test_mock_from_inside_thread_does_not_lock_forever() { - let _mock_outside_thread = mock("GET", "/").with_body("outside").create(); + let mut s = Server::new(); + let host = s.host_with_port(); + let _mock_outside_thread = s.mock("GET", "/").with_body("outside").create(); + let server_mutex = Arc::new(Mutex::new(s)); + let server_clone = server_mutex; let process = thread::spawn(move || { - let _mock_inside_thread = mock("GET", "/").with_body("inside").create(); + let mut s = server_clone.lock().unwrap(); + let _mock_inside_thread = s.mock("GET", "/").with_body("inside").create(); }); process.join().unwrap(); - let (_, _, body) = request("GET /", ""); - + let (_, _, body) = request(&host, "GET /", ""); assert_eq!("outside", body); } #[test] fn test_head_request_with_overridden_content_length() { - let _mock = mock("HEAD", "/") + let mut s = Server::new(); + let host = s.host_with_port(); + let _mock = s + .mock("HEAD", "/") .with_header("content-length", "100") .create(); - let (_, headers, _) = request("HEAD /", ""); + let (_, headers, _) = request(&host, "HEAD /", ""); - assert_eq!(vec!["connection: close", "content-length: 100"], headers); + assert_eq!( + vec!["connection: close", "content-length: 100"], + headers[0..=1] + ); } #[test] fn test_propagate_protocol_to_response() { - let _mock = mock("GET", "/").create(); + let mut s = Server::new(); + let host = s.host_with_port(); + let _mock = s.mock("GET", "/").create(); - let stream = request_stream("1.0", "GET /", "", ""); - stream.shutdown(Shutdown::Write).unwrap(); + let stream = request_stream("1.0", &host, "GET /", "", ""); let (status_line, _, _) = parse_stream(stream, true); assert_eq!("HTTP/1.0 200 OK\r\n", status_line); @@ -1246,12 +1552,14 @@ fn test_propagate_protocol_to_response() { #[test] fn test_large_body_without_content_length() { + let mut s = Server::new(); + let host = s.host_with_port(); let body = "123".repeat(2048); - let _mock = mock("POST", "/").match_body(body.as_str()).create(); + let _mock = s.mock("POST", "/").match_body(body.as_str()).create(); - let stream = request_stream("1.0", "POST /", "", &body); - stream.shutdown(Shutdown::Write).unwrap(); + let headers = format!("content-length: {}\r\n", body.len()); + let stream = request_stream("1.0", &host, "POST /", &headers, &body); let (status_line, _, _) = parse_stream(stream, false); assert_eq!("HTTP/1.0 200 OK\r\n", status_line); @@ -1259,14 +1567,23 @@ fn test_large_body_without_content_length() { #[test] fn test_transfer_encoding_chunked() { - let _mock = mock("POST", "/") + let mut s = Server::new(); + let host = s.host_with_port(); + let _mock = s + .mock("POST", "/") .match_body("Hello, chunked world!") .create(); let body = "3\r\nHel\r\n5\r\nlo, c\r\nD\r\nhunked world!\r\n0\r\n\r\n"; let (status, _, _) = parse_stream( - request_stream("1.1", "POST /", "Transfer-Encoding: chunked\r\n", body), + request_stream( + "1.1", + &host, + "POST /", + "Transfer-Encoding: chunked\r\n", + body, + ), false, ); @@ -1275,116 +1592,156 @@ fn test_transfer_encoding_chunked() { #[test] fn test_match_exact_query() { - let _m = mock("GET", "/hello") + let mut s = Server::new(); + let host = s.host_with_port(); + let _m = s + .mock("GET", "/hello") .match_query(Matcher::Exact("number=one".to_string())) .create(); - let (status_line, _, _) = request("GET /hello?number=one", ""); + let (status_line, _, _) = request(&host, "GET /hello?number=one", ""); assert_eq!("HTTP/1.1 200 OK\r\n", status_line); - let (status_line, _, _) = request("GET /hello?number=two", ""); - assert_eq!("HTTP/1.1 501 Mock Not Found\r\n", status_line); + let (status_line, _, _) = request(&host, "GET /hello?number=two", ""); + assert_eq!("HTTP/1.1 501 Not Implemented\r\n", status_line); } #[test] fn test_match_exact_query_via_path() { - let _m = mock("GET", "/hello?number=one").create(); + let mut s = Server::new(); + let host = s.host_with_port(); + let _m = s.mock("GET", "/hello?number=one").create(); - let (status_line, _, _) = request("GET /hello?number=one", ""); + let (status_line, _, _) = request(&host, "GET /hello?number=one", ""); assert_eq!("HTTP/1.1 200 OK\r\n", status_line); - let (status_line, _, _) = request("GET /hello?number=two", ""); - assert_eq!("HTTP/1.1 501 Mock Not Found\r\n", status_line); + let (status_line, _, _) = request(&host, "GET /hello?number=two", ""); + assert_eq!("HTTP/1.1 501 Not Implemented\r\n", status_line); } #[test] fn test_match_partial_query_by_regex() { - let _m = mock("GET", "/hello") + let mut s = Server::new(); + let host = s.host_with_port(); + let _m = s + .mock("GET", "/hello") .match_query(Matcher::Regex("number=one".to_string())) .create(); - let (status_line, _, _) = request("GET /hello?something=else&number=one", ""); + let (status_line, _, _) = request(&host, "GET /hello?something=else&number=one", ""); assert_eq!("HTTP/1.1 200 OK\r\n", status_line); } #[test] fn test_match_partial_query_by_urlencoded() { - let _m = mock("GET", "/hello") + let mut s = Server::new(); + let host = s.host_with_port(); + let _m = s + .mock("GET", "/hello") .match_query(Matcher::UrlEncoded("num ber".into(), "o ne".into())) .create(); - let (status_line, _, _) = request("GET /hello?something=else&num%20ber=o%20ne", ""); + let (status_line, _, _) = request(&host, "GET /hello?something=else&num%20ber=o%20ne", ""); assert_eq!("HTTP/1.1 200 OK\r\n", status_line); - let (status_line, _, _) = request("GET /hello?something=else&number=one", ""); - assert_eq!("HTTP/1.1 501 Mock Not Found\r\n", status_line); + let (status_line, _, _) = request(&host, "GET /hello?something=else&number=one", ""); + assert_eq!("HTTP/1.1 501 Not Implemented\r\n", status_line); } #[test] fn test_match_partial_query_by_regex_all_of() { - let _m = mock("GET", "/hello") + let mut s = Server::new(); + let host = s.host_with_port(); + let _m = s + .mock("GET", "/hello") .match_query(Matcher::AllOf(vec![ Matcher::Regex("number=one".to_string()), Matcher::Regex("hello=world".to_string()), ])) .create(); - let (status_line, _, _) = request("GET /hello?hello=world&something=else&number=one", ""); + let (status_line, _, _) = request( + &host, + "GET /hello?hello=world&something=else&number=one", + "", + ); assert_eq!("HTTP/1.1 200 OK\r\n", status_line); - let (status_line, _, _) = request("GET /hello?hello=world&something=else", ""); - assert_eq!("HTTP/1.1 501 Mock Not Found\r\n", status_line); + let (status_line, _, _) = request(&host, "GET /hello?hello=world&something=else", ""); + assert_eq!("HTTP/1.1 501 Not Implemented\r\n", status_line); } #[test] fn test_match_partial_query_by_urlencoded_all_of() { - let _m = mock("GET", "/hello") + let mut s = Server::new(); + let host = s.host_with_port(); + let _m = s + .mock("GET", "/hello") .match_query(Matcher::AllOf(vec![ Matcher::UrlEncoded("num ber".into(), "o ne".into()), Matcher::UrlEncoded("hello".into(), "world".into()), ])) .create(); - let (status_line, _, _) = request("GET /hello?hello=world&something=else&num%20ber=o%20ne", ""); + let (status_line, _, _) = request( + &host, + "GET /hello?hello=world&something=else&num%20ber=o%20ne", + "", + ); assert_eq!("HTTP/1.1 200 OK\r\n", status_line); - let (status_line, _, _) = request("GET /hello?hello=world&something=else", ""); - assert_eq!("HTTP/1.1 501 Mock Not Found\r\n", status_line); + let (status_line, _, _) = request(&host, "GET /hello?hello=world&something=else", ""); + assert_eq!("HTTP/1.1 501 Not Implemented\r\n", status_line); } #[test] fn test_match_query_with_non_percent_url_escaping() { - let _m = mock("GET", "/hello") + let mut s = Server::new(); + let host = s.host_with_port(); + let _m = s + .mock("GET", "/hello") .match_query(Matcher::AllOf(vec![ Matcher::UrlEncoded("num ber".into(), "o ne".into()), Matcher::UrlEncoded("hello".into(), "world".into()), ])) .create(); - let (status_line, _, _) = request("GET /hello?hello=world&something=else&num+ber=o+ne", ""); + let (status_line, _, _) = request( + &host, + "GET /hello?hello=world&something=else&num+ber=o+ne", + "", + ); assert_eq!("HTTP/1.1 200 OK\r\n", status_line); } #[test] fn test_match_missing_query() { - let _m = mock("GET", "/hello").match_query(Matcher::Missing).create(); + let mut s = Server::new(); + let host = s.host_with_port(); + let _m = s + .mock("GET", "/hello") + .match_query(Matcher::Missing) + .create(); - let (status_line, _, _) = request("GET /hello?", ""); + let (status_line, _, _) = request(&host, "GET /hello?", ""); assert_eq!("HTTP/1.1 200 OK\r\n", status_line); - let (status_line, _, _) = request("GET /hello?number=one", ""); - assert_eq!("HTTP/1.1 501 Mock Not Found\r\n", status_line); + let (status_line, _, _) = request(&host, "GET /hello?number=one", ""); + assert_eq!("HTTP/1.1 501 Not Implemented\r\n", status_line); } #[test] fn test_anyof_exact_path_and_query_matcher() { - let mock = mock( - "GET", - Matcher::AnyOf(vec![Matcher::Exact("/hello?world".to_string())]), - ) - .create(); + let mut s = Server::new(); + let host = s.host_with_port(); + let mock = s + .mock( + "GET", + Matcher::AnyOf(vec![Matcher::Exact("/hello?world".to_string())]), + ) + .create(); - let (status_line, _, _) = request("GET /hello?world", ""); + let (status_line, _, _) = request(&host, "GET /hello?world", ""); assert_eq!("HTTP/1.1 200 OK\r\n", status_line); mock.assert(); @@ -1392,17 +1749,26 @@ fn test_anyof_exact_path_and_query_matcher() { #[test] fn test_default_headers() { - let _m = mock("GET", "/").create(); + let mut s = Server::new(); + let host = s.host_with_port(); + let _m = s.mock("GET", "/").create(); - let (_, headers, _) = request("GET /", ""); - assert_eq!(vec!["connection: close", "content-length: 0"], headers); + let (_, headers, _) = request(&host, "GET /", ""); + assert_eq!(3, headers.len()); + assert_eq!( + vec!["connection: close", "content-length: 0"], + headers[0..=1] + ); + let date_parts: Vec<&str> = headers[2].split(':').collect(); + assert_eq!("date", date_parts[0]); } #[test] fn test_missing_create_bad() { testing_logger::setup(); - let m = mock("GET", "/"); + let mut s = Server::new(); + let m = s.mock("GET", "/"); drop(m); // Expecting one warning @@ -1425,7 +1791,8 @@ fn test_missing_create_bad() { fn test_missing_create_good() { testing_logger::setup(); - let m = mock("GET", "/").create(); + let mut s = Server::new(); + let m = s.mock("GET", "/").create(); drop(m); // No warnings should occur @@ -1442,13 +1809,16 @@ fn test_missing_create_good() { #[test] fn test_same_endpoint_different_responses() { - let mock_200 = mock("GET", "/hello").with_status(200).create(); - let mock_404 = mock("GET", "/hello").with_status(404).create(); - let mock_500 = mock("GET", "/hello").with_status(500).create(); + let mut s = Server::new(); + let host = s.host_with_port(); + + let mock_200 = s.mock("GET", "/hello").with_status(200).create(); + let mock_404 = s.mock("GET", "/hello").with_status(404).create(); + let mock_500 = s.mock("GET", "/hello").with_status(500).create(); - let response_200 = request("GET /hello", ""); - let response_404 = request("GET /hello", ""); - let response_500 = request("GET /hello", ""); + let response_200 = request(&host, "GET /hello", ""); + let response_404 = request(&host, "GET /hello", ""); + let response_500 = request(&host, "GET /hello", ""); mock_200.assert(); mock_404.assert(); @@ -1461,18 +1831,22 @@ fn test_same_endpoint_different_responses() { #[test] fn test_same_endpoint_different_responses_last_one_forever() { - let _mock_200 = mock("GET", "/hello").with_status(200).create(); - let _mock_404 = mock("GET", "/hello").with_status(404).create(); - let _mock_500 = mock("GET", "/hello") + let mut s = Server::new(); + let host = s.host_with_port(); + + let _mock_200 = s.mock("GET", "/hello").with_status(200).create(); + let _mock_404 = s.mock("GET", "/hello").with_status(404).create(); + let _mock_500 = s + .mock("GET", "/hello") .expect_at_least(1) .with_status(500) .create(); - let response_200 = request("GET /hello", ""); - let response_404 = request("GET /hello", ""); - let response_500_1 = request("GET /hello", ""); - let response_500_2 = request("GET /hello", ""); - let response_500_3 = request("GET /hello", ""); + let response_200 = request(&host, "GET /hello", ""); + let response_404 = request(&host, "GET /hello", ""); + let response_500_1 = request(&host, "GET /hello", ""); + let response_500_2 = request(&host, "GET /hello", ""); + let response_500_3 = request(&host, "GET /hello", ""); assert_eq!(response_200.0, "HTTP/1.1 200 OK\r\n"); assert_eq!(response_404.0, "HTTP/1.1 404 Not Found\r\n"); @@ -1483,21 +1857,127 @@ fn test_same_endpoint_different_responses_last_one_forever() { #[test] fn test_matched_bool() { - let m = mock("GET", "/").create(); + let mut s = Server::new(); + let host = s.host_with_port(); + let m = s.mock("GET", "/").create(); - let (_, _, _) = request_with_body("GET /", "", ""); + let (_, _, _) = request_with_body(&host, "GET /", "", ""); m.assert(); assert!(m.matched(), "matched method returns correctly"); - let (_, _, _) = request_with_body("GET /", "", ""); + let (_, _, _) = request_with_body(&host, "GET /", "", ""); assert!(!m.matched(), "matched method returns correctly"); } #[test] fn test_invalid_header_field_name() { - let _m = mock("GET", "/").create(); + let mut s = Server::new(); + let host = s.host_with_port(); + let _m = s.mock("GET", "/").create(); + + let (uppercase_status_line, _, _body) = request(&host, "GET /", "Bad Header: something\r\n"); + assert_eq!("HTTP/1.1 400 Bad Request\r\n", uppercase_status_line); +} + +#[test] +fn test_running_multiple_servers() { + let mut s1 = Server::new(); + let mut s2 = Server::new(); + let mut s3 = Server::new(); + + let _m = s2.mock("GET", "/").with_body("s2").create(); + let _m = s1.mock("GET", "/").with_body("s1").create(); + let _m = s3.mock("GET", "/").with_body("s3").create(); + + let (_, _, body1) = request_with_body(&s1.host_with_port(), "GET /", "", ""); + let (_, _, body2) = request_with_body(&s2.host_with_port(), "GET /", "", ""); + let (_, _, body3) = request_with_body(&s3.host_with_port(), "GET /", "", ""); + + assert!(s1.host_with_port() != s2.host_with_port()); + assert!(s2.host_with_port() != s3.host_with_port()); + assert_eq!("s1", body1); + assert_eq!("s2", body2); + assert_eq!("s3", body3); +} + +#[test] +fn test_running_lots_of_servers_wont_block() { + let mut s1 = Server::new(); + let _s2 = Server::new(); + let _s3 = Server::new(); + let _s4 = Server::new(); + let _s5 = Server::new(); + let _s7 = Server::new(); + let _s8 = Server::new(); + let _s9 = Server::new(); + let _s10 = Server::new(); + let _s11 = Server::new(); + let _s12 = Server::new(); + let _s13 = Server::new(); + let _s14 = Server::new(); + let _s15 = Server::new(); + let _s17 = Server::new(); + let _s18 = Server::new(); + let _s19 = Server::new(); + let _s20 = Server::new(); + let _s21 = Server::new(); + let _s22 = Server::new(); + let _s23 = Server::new(); + let _s24 = Server::new(); + let _s25 = Server::new(); + let _s27 = Server::new(); + let _s28 = Server::new(); + let _s29 = Server::new(); + let mut s30 = Server::new(); + + let m1 = s1.mock("GET", "/pool").create(); + let (_, _, _) = request_with_body(&s1.host_with_port(), "GET /pool", "", ""); + m1.assert(); + + let m30 = s30.mock("GET", "/pool").create(); + let (_, _, _) = request_with_body(&s30.host_with_port(), "GET /pool", "", ""); + m30.assert(); +} + +#[tokio::test] +async fn test_http2_requests_async() { + let mut s = Server::new_async().await; + let m1 = s.mock("GET", "/").with_body("test").create_async().await; + + let (status, _headers, body) = + hyper_request(&s.host_with_port(), "2.0", "GET", "/", None).await; + assert_eq!(200, status); + assert_eq!("test", body); + + m1.assert_async().await; +} + +#[tokio::test] +async fn test_simple_route_mock_async() { + let mut s = Server::new_async().await; + let _m1 = s + .mock("GET", "/hello") + .with_body("world") + .create_async() + .await; + + let (status, _headers, body) = + hyper_request(&s.host_with_port(), "1.1", "GET", "/hello", None).await; + assert_eq!(200, status); + assert_eq!("world", body); +} + +#[tokio::test] +async fn test_two_route_mocks_async() { + let mut s = Server::new_async().await; + let _m1 = s.mock("GET", "/a").with_body("aaa").create_async(); + let _m2 = s.mock("GET", "/b").with_body("bbb").create_async(); - let (uppercase_status_line, _, body) = request("GET /", "Bad Header: something\r\n"); - assert_eq!("HTTP/1.1 422 Mock Error\r\n", uppercase_status_line); - assert_eq!(body, httparse::Error::HeaderName.to_string()) + let (_m1, _m2) = futures::join!(_m1, _m2); + + let (_, _, body_a) = hyper_request(&s.host_with_port(), "1.1", "GET", "/a", None).await; + assert_eq!("aaa", body_a); + + let (_, _, body_b) = hyper_request(&s.host_with_port(), "1.1", "GET", "/b", None).await; + assert_eq!("bbb", body_b); }